If you're unsatisfied in the first week, cancel for a full refund.
Enter your email to preview the syllabus.
You'll master these fundamentals with 1-on-1 guidance from your mentor who is a professional developer. Your mentor will be there to help you when you get stuck, motivate you to finish a challenging project, and keep you on track. You’ll graduate with a full portfolio and a fully-stacked GitHub profile. Even if you’ve literally never coded a single line you can take the course and become a beginner frontend developer.
Learn with Thinkful's free programming tutorials and resources. You'll gain access to tutorials on HTML, jQuery, Angular & more.
So what is isomorphism? The goal of isomorphism is to write a piece of code once and have it run on both the client and the server. We do this so that we have less code to debug, and so we can still have all of our UI being rendered on the server so that it's immediately available to the user on a page refresh (and obviously for SEO reasons on public-facing pages).
For those of you who don't know React: React is a client-side framework built by Facebook that allows you to build web components that are "reactive." Reactive means that rather than manually controlling the visual appearance of your data, you just alter your data, re-render the entire component, and then React takes care of the magic diffing that makes that all work in anything resembling a performant fashion.
So what React lets you do is break down your UIs into multiple components. Components can be nested, so you can have a list which has multiple items, and each of those is an independent React component that maintains its entire state by itself.
The problem is: in web applications, a lot of what we're doing is asynchronous. We're making database calls, we're making API calls — those have to ping the server and come back. This isn't really built into the ability to render a React component to a string. The React renderToString method is provided for you but it's synchronous; and what's worse is if you're using react-router — which you almost certainly are in order to maintain state as you navigate through your overall application — that also does not handle asynchronous data calls very well.
So what don't we want to do? We don't want to break separation of concerns and we don't want to break isomorphism.
Breaking separation of concerns would be something like this: let's say we have an endpoint where you're displaying a set of questions. It's pretty reasonable for you as the developer to think: "Oh, well, this endpoint and this handler in Express would easily be able to go fetch the questions and provide them to React." But the problem is that the React component itself also needs to know how to do that, because when you make changes on the client-side that can be done in place — and so that duplication of data calls breaks separation of concerns, and that's bad.
The other thing we don't want to do is break isomorphism. If we litter our code with a bunch of "IF client ELSE IF server" calls, then we've destroyed the whole point of making this all isomorphic and running the same on both environments. We have twice the amount of code to test, and it means that we have code that's often duplicative in many ways, so that's not the right way to do it.
React can declare statics, which are just the same as static methods on an object. So what we've done here is we've provided two types of statics. One is promises, which gets passed in the state which we know on the server and on the client, and what the component returns is a promise to get data. Essentially, this is the component saying: "in order for me to render the given state, I'm going to need you to fetch me this data asynchronously."
The other thing we do is specify child components. This is slightly duplicative with the render method of the components, but rather than render twice instead we have the components each declare any children that need to also be checked to see if they have any static promises that need to fulfill.
A middleware stack is simply the idea that in order to handle a request that comes into the server, you can layer a stack of functions, each of which takes the incoming request and response to be sent, and then the next function which essentially says "pass off to the next piece of middleware."
So we have a little piece of Express middleware that does the following: it queries all the components for their promises and resolves them; it takes that data and injects it back into the instances of the components so they can render themselves to a string that we can draw on the server; and then the last thing is it dehydrates the data so that it can be passed to the client so the client doesn't have to repeat all those data calls.
What does this look like in the end? It looks like this: so we have our Questions admin app with a list of items, and we have a detail view where you can edit each item. And as you can see when we refresh, nothing changes. The only things that flicker are the image and the fonts which we haven't cached (since it's an admin app we want to make sure nothing's caching).
And you can see that now, this is a fully fledged React component — because when I change the question that I'm viewing, the only things that changed are the items that have updated that are different between the two different questions. You don't see the entire section of the page redraw. In this case, only the image and the question text change whereas the items on the right, which are the same for each of these questions, don't change.
No spam ever. We promise!
Sign up for a weekly update on upcoming sessions
Now, as a result, today I'm going to be talking about a better way to handle Flux in large complex applications. In summary, we overall found that when you have a ton of inter-connected stores, keeping a clean declarative pattern quickly becomes difficult. Our solution was to centralize all of our data in one top level store and make those stores hierarchical.
But first, let me introduce Flux. Now for those of you who've built an app with Flux before, the next couple of slides should be reivew. To get us all on the same page, let me show you how this works with a simple example. So let's say we wanted to build a list of email messages with React and Flux.
Now this is Flux. It is a design pattern. It is a cleaner, more declarative way to manage your data. Now there are many libraries you may have heard of that offer implementations of Flux — for example, Fluxxor, Reflux, and Facebook's own FluxJS. But for the sake of this talk we'll only be referring to this design pattern.
At the core of Flux and this talk are stores. Stores hold your data. In our example it holds a bunch of email messages. Stores are not models in the traditional MVC sense. The first thing to remember and know about them is that in the Flux design pattern, stores are singletons — there's only one. In this case, the MessageStore's job is to cache and aggregate my messages in meaningful ways vend ideally immutable copies of that data through public getters, and listen for actions that may cause data to change.
So next, this is the React component. Any time that MessageStore changes, it notifies the Component. The component then gets fresh data from the MessageStore and renders it.
If a user wants to interact with the component — like deleting a message — the Component will fire an action that’s dispatched through the central dispatcher and caught by the store. Upon receiving the action, the store will update its own internal data, then “trigger” to let everyone know the data has changed. Upon trigger, the component will then fetch new data and the cycle is complete.
This is the key pattern that keeps the view declaratively bound to its data. More importantly it ensures that the view never gets out of sync. The view is always an accurate representation of the data. If the data were to ever change, the MessageStore would trigger. When the MessageStore triggers, the view re-fetches its latest data.
This “action to trigger to refresh pattern” is the central dogma of Flux and guides the creation of simple applications. However, we ended up finding that while it works for simpler applications, this pattern does start to get messier and messier the more complex an app gets.
If you have a relatively small amount of data or your data is always completely independent of other objects in your application then great! You can stop listening to this talk now. Unfortunately in large, complex applications this interdependency becomes unavoidable.
Let’s take a look at what happens to our initially simple Message List example as our requirements become more complex.
So let's say the first thing we want to do is update the current list of messages whenever the actively selected thread changes. Now, instead of having an imperative method to swap out the thread, I’m going to wire up the MessageList to be declaratively linked to a ThreadStore. This way whenever the ThreadStore changes with the newly selected thread, that MessageStore will update itself with the correct data.
What you see here is a store listening to another store, and that's actually a very complex design pattern in Flux. And the minute you have this interdependent data, this pattern will emerge. In this example it’s relatively manageable and clean, but we’ve found it quickly gets a lot messier.
Ok, so now say I can display the list of email messages in the context of a thread, but now I want to reply to those messages and I want to display the current draft that I'm working on in-line with the rest of the conversation. One reasonable way to do this is to have my MessageStore listen to changes in another singleton DraftStore. Whenever a new draft is created or destroyed the DraftStore updates. When the DraftStore updates, the MessageStore will fetch the appropriate data and update itself. When the MessageStore updates, the View will re-fetch the data and re-render. The net result is that the message list will now declaratively represent the state of the MessageStore, the DraftStore AND the ThreadStore.
So now, let's say I want to add in a websocket connection to my app because I like to get live updates from my server. This is great because when I get a new email message it can instantly show up at the bottom of my message list. So if this were jQuery land, we'd probably do something like call `append` and put a new DOM element whenever something comes in. However, we can't do this anymore — this would break our nice declarative pattern, because a new set of headaches would emerge with jQuery spaghetti code. Instead I’m going to listen for whenever the websocket changes and then declaratively fetch new information from a cache of new data.
And now finally, just to add even mroe complexity, say we want to support multiple languages and have the concept of a translation module that will help us display the messages display the messages based on the current language. Once again, we listen to yet another store, this time a LanguageStore, to declaratively get the appropriate data for the message list.
So let’s take a step back and look at where we’ve gotten ourselves. We have a MessageStore that’s now wired up to the current state of several other stores. That was just one Store! Each of those other stores might in turn be wired up to more stores.
Now we have a problem. This is hard to reason about.
When a change happens somewhere on the system, where will it will propagate to? There might be hidden circular references. These chains of trigger-patterns may be arbitrarily long. And furthermore, the ordering of them probably starts to matter!
This problem is not new. In fact, Facebook’s own Flux website has this hairy bit of forewarning about the nature of complex applications. They admit to the fact that these interdependencies happen. In fact, they have this concept of waitsFor that's pretty brittle because of the interdependent nature and timing of all the actions around a store loop.
So instead of getting ourselves into interdependent hell, we found one, initially scary, but eventually elegant solution to fix a lot of these problems.
What we did is we centralized all of our data in one top-level store. This is the global, singular, DataStore. It houses all of the data and is crucially the single source of truth in the application. It makes the dependency diagram go from this — to this.
There are no more cycles. No more arrow spaghetti. No more redundant listeners. This diagram is much easier to reason about, and scales better. It decouples previously dependent stores from each other and leads to a cleaner, more isolated system. Most of the stores in the app will listen to this DataStore. Whenever the DataStore triggers, the listening stores re-fetch their data from this central repository and, if necessary, trigger themselves.
When the stores re-fetch their data from the central DataStore, they can now pull together whatever disparate data they need to fulfill their request. This is how we easily satisfy stores that need composite data. Instead of fetching data from a myriad of other different sources like the MessageStore used to, it can now get it all from one place.
To further illustrate how this helps us, let’s look back at a notorious but annoying problem with Flux: which is this concept of getting new websocket data into the app.
Before we had the centralized DataStore, our MessageStore had to explicitly listen to the websocket or to whatever we're pulling this data from. And all of these individual listeners set up a lot of duplicative code and was one of the contributors to the dependency mess we saw earlier. Furthermore, if the socket streamed a lot of different data, there'd be this routing problem to get all of the data to the right store, especially when we have composite data.
But with a centralized DataStore, the websocket can simply dump all of its composite data right into this repository. The minute that happens, the DataStore will trigger and all listening Stores will correctly fetch the appropriate composite data from the DataStore.
Changing state in the app suddenly becomes easy - data can be written to the data store from anywhere, and changes will propagate to the other stores, and out to the corresponding React components that were listening to those stores.
Now, when we describe this data store as being a singular piece of global shared mutable state, we’d commonly get this reaction.
Anyone who has written parallel code knows how horrible it can be when an object you’re referenced silently and suddenly and inconsistently and it changes underneath you. This tends to be the worst form of Heisenbug and there are tons of patterns designed to prevent this.
Now, our DataStore is global, it is mutable, and it is shared. But it is not evil. One of the biggest reasons it is not evil is because of the immediate triggering mechanism built into the Flux pattern, and that we're not passing its reference under the hood — there's always this getting method from there. In a system, the minute anything in the DataStore changes, that trigger will propagate throughout the whole system and tell everyone else to refresh. Instead of passing data down, everyone gets their own new data cleanly from the store. And furthermore it's good practice to make that fetching immutable. In fact, React systems like Om make it mandatory to fetch immutable data.
This centralized DataStore also then becomes extremely robust to mutating changes. Since everything else in the app, from the views, to the stores, declaratively derives from this data, if you wish to change something in the DataStore, your wish will be granted. A store can itself mutate the dataStore. A websocket can mutate the DataStore. You can even open up the console and change some data manually and know it will safely propagate its way through the app. And because of the trigger refresh mechanisms of listening stores, everything stays in sync.
Now, one of the lasting implications of moving everything to a DataStore like this is that it helped us think about our app in a much more structured and hierarchical way. As our app grew, and the views required composite data, so thinking about what stores needed to hold what data became challenging.
What we realized was that each store was just a cached subset of its parent. The data always stays in sync because of the Flux trigger and refresh pattern, with all the data getting aggregated at the root. So another way to think about it is that the DataStore is just a cached subset of all of the data on the API. And then the MessageStore is just a cached subset that stays in sync of the data in the DataStore. And furthermore, the React component down the line may be another subset of all of that as well.
At each level in the hierarchy, we’re defining this declarative contract to guarantee that a certain subset of composite data at any point will be available for you. We then rely on the trigger/fetch mechanism of Flux to ensure that the data is always in sync, and that this is always true. With this hierarchy, any data entered from the top will propagate down to exactly the stores and components that needs, and will always stay in sync.
One final bonus of a centralized DataStore is that the state of the entire app is in one, savable, place. You could persist it to local storage, you can keep it on a backend, or you can save it for diagnosing crashes. In fact, in the application we’re building our DataStore is a full blown local SQLite Database instance. We subsequently wrote our own ActiveRecord-like ORM interface to set and retrieve data into from. The Database is our single source of truth, and the only place we have to worry about changing to reflect new states in the application.
So a centralized data store is one of the more effective ways we found to reign in the complexity of our increasingly growing application. It’s drastically help simplify the reasoning of our app, kept things decoupled, and led us to think about our data in a hierarchical fashion.
Even if you don’t immediately restructure everything you’re building now, I hope that as your apps grow in complexity and data starts to get intertwined, you’ll remember this as a way to keep your app sane.
Bring your questions about best practices, workflows or integration challenges. Hear about the challenges other developers are running into, or screenshare your own code with the group for feedback.