“Thinking in React” — A paradox statement
This morning I had one of my rare visits on Twitter. While I was an avid tweeter some years ago, I nowadays mostly visit it for tech discussions of the “hottest heads” in my current field of occupation. As I was scrolling through Dan Abramov’s timeline, I found the following tweet.
I was intrigued! A discussion between two of the most community engaging and aspiring devs in the React field. What could go wrong?
Ohhh boy. After hundreds of discussions about the different Flux flavors, completely different approaches like MobX or Baobab (which are still inspired by the Flux pattern), Ryan jumps into the cold water of the React Twitter community and says that exactly this pattern, which was an inspiration for a ton of state management libraries, is a handicap for React innovation. He also followed it up with another tweet:
It was, and still is, all right here. Build your app like this and you won’t be scratching your head in 6 months: https://facebook.github.io/react/docs/thinking-in-react.html
Now this was getting interesting. I read this documentation article when I first started out with React (9 months ago!). But now, reading it again with a few months of experience under the belt, I could not ignore a certain paradox. But before we investigate further, let’s take a step back and think about the three basic principles of React components.
React Components — Three Basic Principles
Other devs and I have identified three principles which should be the foundation for all React components:
- A component should be a pure function which transforms data into a user interface. For a regular React web app, this means that a component takes some data and returns HTML.
- Components should be as generic as possible to promote reusability. To achieve this, you don’t want to bind the components which generate your HTML to a specific state management solution. Instead, you divide your components into container (smart) components and presentational (dumb) components. The smart components extract data from your state without directly generating HTML (and thus can be bound to your state management!), while the dumb components transform that data into HTML.
- A component should be performant. It should only render when its input has changed.
We will ignore the third principle right now (I may come back to it in a later article) and instead focus on the first two. To recap, our dumb components take data and return a UI, while our smart components supply that data to the dumb components. Easy enough, right? But what if multiple dumb components rely on the same data and should display updates to that data synchronously?
Data Flow in React
Let’s take the component tree above as an example to study the way we move data around inside a React application. Imagine we want to pass data from B to A. Sounds easy, right? Just use props!
B passes data to A via props. A can request changes to these data by using a callback which gets passed to A as well!
Now that we got ourselves warmed up, let’s try something different. D and E should display something based on the same data (An example would be a product list on the left of the screen and more detailed information about the clicked product on the right). Applying the “Lifting State Up” patterns makes this a child’s play.
The state is held in the common ancestor of D and E. It passes the data (and callbacks to request changes) down to both of its children. When E requests a change and the data receives an update, we automatically pass the updated data down to both D and E. Thus, D and E are always in sync! Note that we don’t have any kind of coupling between any dumb components. The only component which knows that there is a data relationship between D and E is their immediate parent, which is a smart component.
Ok, this was a little harder, but we didn’t break a sweat. How about passing the same data to C and D? They have a common ancestor in the tree’s root, so we can just apply the “Lifting State Up” pattern. Right?
This works. The root passes data down and all the components between the root and C and D respectively forward the props until they arrive at their destination. There is a problem though. Each component between the root and C has to know that it needs to receive and pass down props without actually consuming them. The same happens for each component between the root and D. This means that we have created a coupling between the target of our data (C and D) and all the components between these targets and their common state container (the root component). If we were to replace C or D with a different component, or supply other data to them, we would need to change all of those coupled components as well. Yikes! And I am not the only one who noticed this problem:
Now this is the point where we have to make a decision. Do we want to keep this coupling between data-providing, -passing, and -consuming components. Probably not, since it impacts our ability to reuse individual snippets of our component tree in other contexts or applications. There is a solution for this problem though, and its name is Context.
Context to the Rescue!
Context allows us to transform the root component into a context provider, which would then provide data for other components further down the tree without having to pass down this data explicitly the whole way to the target components. Instead, our consuming components simply define the data they want to pull from context and receive them automatically. No more coupling and we can easily move our consuming components around the tree as long as we have a context provider for their data somewhere further upstream. Hooray!
This comes with another problem though. A component can only pull data from context when it is running through the update lifecycle phases. If the parent of the context consumer does not rerender (For example by being a PureComponent) our consumer does not update and will not pull the new data from the context. Instead, we should define a subscribe function in the context which the consumer uses when initially mounting. The provider can then notify the consumer directly when the context state updates and bypass the “context shield” of shouldComponentUpdate or PureComponent. You can read more about this in this article of MobX creator Michel Weststrate.
In addition, the React documentation states that Context is an unstable API and recommends using a simple Provider component at the top of your hierarchy and using Higher Order Components to create the consumers which will then pass down the data to the actually consuming dumb components they wrap. This way we can reduce the places where we need to change code if the Context API changes.
The End of the Journey
Now, where does this lead us? You may have noticed that this “Provider at top, HOCs to consume” pattern sounds quite familiar. React-Redux works this way, as does MobX-React or Baobab-React.
We have come back to the beginning. We started “Thinking in React”, approaching the data flow by simply using props to pass data around and lifting state up. At a certain point though, doing this will hurt the “Composability and Reusablity” principles which are the foundation of React’s whole component system! The solution to this is using React’s context, but this will simply lead us back to our known state management solutions.
In conclusion, I don’t think that Flux hampered React innovation. Instead, it is a simple pattern to handle state management in a reliable, traceable way. However, the implementations of this pattern come in lots of different flavors, some of them are very close to the original Flux (like Redux) while some are taking some different steps to accomplish your goals (Baobab for example). In the end, it is up to you to find the right implementation for your very own problems and requirements.