If you’ve stumbled on this article, chances are you are or are considering using React. I can’t blame you; React has a flourishing ecosystem, with many people contributing, and with a lot of great libraries: Flux, React Router, Redux, and React Native just to name a few. With that being said, it’s usage, many tools and solutions, as well as things presented at events (such as Meetups) are in fact contributing to a vicious cycle: approaching a problem in a way that creates another problem, just to do it again. This is not entirely to blame on people picking up React, as the Facebook docs do little to help you see the big picture if you don’t invest quite a bit of time up front.
Suffice to say, I’ve seen some scary things in my short few years using React. There is much to learn about the idealized way to do things just by observing what not to do. And while I am not simply going to make this a “things-not-to-do” list, I would like to start by pointing out something important: The name of this article is a popular misnomer;
React is not an Architecture!
If we were to think of this as the often-idealized Model-View-Controller concept (or MVC), React is only the View; in essence: a thin view library with unidirectional data bindings and its own internal component life-cycle.
Along with the understanding of this fundamental concept, there are 3 guidelines for ideal use of React in an application**:
- Controllers and models are not directly represented by React components
- Props are at most a few levels deep when being funneled from parents
- Data reflected in React components should be cheap to calculate a diff; ideally, they should be immutable (although other techniques such as a hashed-identification prop also work).
** not to be mistaken for the entire application, but just the coupling of React and its abstraction for your view layer
Now with that said — let’s break down why each of these hold true:
Controllers and models should not be represented by React components
Because React Components have their own dynamic state (with the exception of functional Components), it often gets thought of by newcomers as a perfectly suitable place to intrinsically house your data models. Unfortunately, this leads to more trouble than it’s worth.
The Problems with Component state for Data Modeling
The concept of using React Components’ internal state for data modeling in any non-trivial app is flawed for several reasons:
1. Component state updates are less predictable than if we dispatched global action events centrally (e.g. with a Flux-based architecture). There is a massive overhead of React’s life-cycle bindings to refresh and we cannot time those or predict them; they simply happen “when they happen”.
2. Updating state via setState is also asynchronous, so we lose the ability to observe other global data models and update synchronously; and if other components depend on these latent updates for their own state changes, this adds to both unpredictability as well as complexity.
3. State gets lost when a component unmounts/remounts. What good is business logic if it is not predictable or persistent when you navigate away from your current view, anyway? And if you have to have a view pasted to your app at all times, this is also a completely avoidable performance loss to consider.
4. There is a lot of overhead when calculating state updates as opposed to stateless components (though we should still be optimizing these with recompose when possible).
5. Stateless components are defined as a simple pure function and are the most clean, predictable and declarative way that a component can be written today.
6. Business logic should not be mutating as an exclusive effect of a view re-render; the view layer should reflects changes in our data model, rather than a data model which is changed (or is controlled) at the whims of when the browser decides to complete a DOM reflow/repaint cycle.
7. When you are debugging a large team-oriented application, component state is a hindrance when dealing with Other People’s Code (OPC) — asynchronously tracing data flow to figure out what is going on. Components contain layout information in addition to DOM bindings, and so adding in any additional render-coupled business logic gets cumbersome in a scaling application.
In essence, the solution to where to put your data model goes back to the idea of “React is not an application architecture”: it will not take the place of a good framework to model your business logic and data mutations. There are several out there — the one with most notoriety is Redux; but this comes down to your team’s requirements.
Having your data model in a separate place along with business logic is crucial to creating a sane React application.
Components should only be concerned with data via observed props that are dispatched to the component in a single direction; user interactions should be managed by actions being dispatched to a system that can then notify other components.
When it actually makes sense to use a React Component’s State
So with that, React Components’s state should exist for a reason right? Indeed, they are not just there to hog CPU cycles, make your debug stack traces less friendly and make you create architectueres that require debugging through quantum space-time. Updating state provides a hook for when we can re-render a Component. Sometimes we explicitly want to re-render not only based on a prop passed down to a component, but also when state changes. Here are a set of criteria when it is reasonable to use internal state in a React Component:
- Your component needs to animate and trigger CSS class changes after certain intervals.
- You’d like to programmatically change a superficial UI that does not require business logic or other components that depend on it (e.g. timing after a mouse event, or to disable pointer events when a permutation of props is met when a component will update, cycling through different parts of an animation).
- If the component’s state is insanely trivial and related to the UI, and there would be no consequences or interaction with any other components. For example: having a menu that has a drop down; Losing the state of whether that was opened may not make a difference since you probably would want to assign the default state to the menu being closed But in some cases such as in a highly interactive UI on an SPA, this may not be true either!
- If the component is designed to be part of a standalone library and it isn’t worth engineering the boilerplate associated: for example a file uploader or an animation tool. In this case, it is worth the added complexity for debugging and maintenance so that the component stays self-contained in other architectures.
So now that we’ve seen the proper use of state, let’s turn to the bread and butter of our React views that glue things together: props. Props are essentially the “unidirectional data flow” we were talking about earlier. And just like with state, we have some abuses found that just don’t lend themselves to scalability to be wary of.
Props should be at most a few levels deep when being funneled from parents
- If the data model changes that a prop is listening to, the component connected must be changed to match.
- If the props for a component in the middle of a set of components funneling a prop from each other (in our above example, Component B), then all components funneling props to the next have their relationship and functionality broken.
- The deeper we nest props, the harder to change a component’s interface without creating a high level of technical debt for your team.
Now that we’ve pointed out what to look out for with props, we can discuss the last point.
Design your data in a way that is cheap to calculate the difference; Use Immutability
This last aspect we’ll be covering about using React is one of the most important to realize; to elaborate: the entire backbone of why React was created ties into this point.
In the browser, DOM re-rendering (specifically repaint and reflow) is one of the most expensive things performance wise to do on a standard Single Page App; React’s virtual DOM allows us to diff and only update particular components as necessary while making it intuitive for the end user with Components. The fact that it does it in a way that approaches the future Web Components standards and paradigm is only a nice side effect and shows the ingenuity/consideration of the team behind the tech.
The way we can selectively apply re-updates ties into the React Component lifecycle — specifically, the shouldComponentUpdate method. shouldComponentUpdate basically calculates whether a component should update by comparing its previous props to its current props; so ideally for best performance and simplicity, every component should have either 1 of 2 characteristics:
- an immutable set of props
- a prop passed to it which serves as a unique hash or id to diff against another component
If a component (1) has an immutable set of props, we can simply make the component an instance of PureComponent (which if you’re using Redux to link your architecture to React, is implicitly done by wrapping a component in connect), which basically says if any prop or state changes from a shallow sense, that component updates. But using a PureComponent also has an important corollary: there is no update if the component’s prop has some inner variables changed. This second point is extremely important to understand; reason being:
- If a component updates, we want it to render ASAP. Checking these properties from a “shallow” perspective (e.g. did the reference change if it is an object) is much cheaper and almost free compared to deep diffing each individual prop.
- If we change a prop object but not its reference; e.g. prop a.b was 1 and gets set to 2 on an update, it will not re-render! This is critical to keep in mind while debugging if your components don’t update.
The second way to speed up diffing with your component updates mentioned — using a hash or id, is less ideal, but also works well. If your component has a unique id or hash for diffing, we only need to check if that specific prop has changed in shouldComponentUpdate. This is actually even faster than a shallow prop comparison, but it is usually both harder to maintain and also easier to make a mistake with or abuse on larger teams with tight deadlines.
Wrapping Things Up
With all that said, I have made a deliberate attempt not to include specific solutions to the architecture side of things as that would involve a much larger write-up in itself. There are a lot of open source libraries for architectures which complement React; particularly any that implement a global action dispatcher system (or basically a glorified pub/sub pattern for observing store data changes at discrete times in your view layer).
Redux is most popular but tends to abstract a few steps such as creating a dispatcher or having a clear separation between how the data/store updates. Because of this, I would recommend beginners go with a more base Flux library which has you handle things more discretely so you can learn to walk before you fly (an example of such is Facebook’s original Flux library). This really helps to get a feeling for a good architecture — most importantly: to create a scalable app off the bat due to less user-error and more verbosity. Not to mention, the architecture can evolve as time permits which is better for business-related learning-on-the-fly. For the more experienced React devs, Redux is probably the way to go thanks to its diverse ecosystem and opinionated choices that mesh well with React (e.g. PureComponents by default, more-direct prop connections to data model via connect/mapStateToProps, etc).
In any case, hopefully this article has been helpful in avoiding some of the mistakes that I have seen and done myself while wrapping my head around creating scalable React apps. There are many opposing philosophies on the topic, so I am curious about your thoughts and approach. Feel free to sound off in the comments below and thanks for reading.