Scaling State: Surrender, Just Rerender
Large web applications like Facebook or even the AddThis dashboard contain an enormous amount of state. Changes to state come from different sources, primarily, updates from the server or user interaction. Each change in state, no matter how small, has the potential to propagate through the entire application. With state change coming from different directions and having app-wide implications, maintaining consistency is a challenge.
So, how might we address maintaining consistency? Well, we might try to have a listener that performs updates to the affected DOM elements whenever the state changes. While in theory this will work, in practice it is verbose and error prone. The onus is on the developer to make sure all of the necessary DOM elements get updated properly, which is difficult to do because an individual developer may not know or remember all of the implications of a single state change.
Once the frustration from hunting bugs becomes too much, your instinct might be to throw your hands up and rerender the whole view every time the state changes. Good news! You’re not alone. This is the approach taken by several frameworks including React.js. Now we can define our view declaratively, and when the underlying data changes, we wipe it away and start fresh. This is starting to sound really nice, but before we start celebrating, there is another problem: we are re-rendering an entire page worth of DOM elements every time anything changes.
Scaling the View: Don’t Mess with DOM
The DOM, or document object model, is slow. More accurately, manipulating the DOM is slow. This is a pretty well known and oft repeated mantra among web developers, but it remains true. It stems from the fact that changes to any element in the DOM’s tree structure could potentially result in the browser having to completely layout the page again. This is a problem when we have large views and/or make frequent changes to the data model (and thus have to re-render frequently).
Don’t despair though, React can help with this problem too. React keeps a “virtual DOM” and tracks changes here. The virtual DOM is lightweight and blazing fast. Once the changes have been accumulated in the virtual DOM, React determines the minimum set of changes it must make on the real DOM, and uses optimizations to make these changes as efficiently as possible. This approach also allows React to batch writes to the DOM to further minimize the number of slow DOM interactions.
Scaling Your Team: Concurrency Through Components
Our dashboard that we recently converted to React+Flux was previously written in Backbone. Famously unopinionated, Backbone provides a team of developers ample flexibility. As the team and project grows, this flexibility has a tendency to morph into disorganization and lack of shared best practices. Specifically, views can become tightly coupled to data sources, business logic can creep in where it does not belong, and the router can grow out of control. Not only does this result in messy and hard to maintain code, it can also lead to stepping on toes and difficult to track down bugs.
<Card> <CardHeader> Wow </CardHeader> <CardBody> So simple. </CardBody> </Card>
I’ll note that this level of organized concurrent development and modularization is not something that couldn’t be achieved in another framework (such as Backbone). In fact, there are many teams out there who do this effectively. However, React+Flux is just prescriptive enough that this level of efficiency can be achieved with little effort.
Scaling Architecture: Enter Flux
Photo by krawaller
In a large MVC application it may be difficult to articulate lines of communication. A controller can pass data to the view, but might also receive user input back from the view. Maybe a change in a view also changes an adjacent view, and so on and so forth. Before we know it, there are data flow arrows pointing in every direction. This (lack of) structure can be difficult to reason about and even more difficult to debug.
Flux mandates unidirectional data flow. This directive is perhaps its defining characteristic. User interaction with a view creates an action, which is “dispatched” to data “stores” that in turn emit changes to the views. With data flow defined in one direction, events can easily be followed through the application by a developer—even one who is unfamiliar with this particular part of the application. Sanity, speed, and developer happiness quickly follow.
Flux does its best to reduce shared mutable state. Flux “stores” (almost analogous to “models”) do not allow outsiders to modify their state. By not exporting their “setters” they make these methods private to the store itself. The setters only get called in the store’s callback registered with action dispatchers. The end result of this design pattern is that bugs are much easier to track down, because any time the store is changing it is happening in a known place.
Scaling Your Brain: Best Practices, Patterns, and Predictability
We learned, however, that perhaps the most important takeaways are tech agnostic. Establish best practices, patterns, and conventions. Adopt them early and reinforce them often. Create a collaborative environment where the best practices are constantly discussed and improved upon. When everyone helps to create conventions, everyone is an owner and takes pride in enforcing them. We are now developing new features for our dashboard faster than ever and this way of thinking and developing has been crucial to setting that pace.