Context is a very powerful feature in React, with lots of disclaimers around it. A bit like the forbidden fruit in paradise.
That should be enough to keep you far away from context right? Well of course not, it is a (unsupported) React feature, and forbidden features will be used for the mere fact that they exist! Context makes it possible to pass data to components deep in the component tree without needing intermediate components to know about it. Classic use cases for context are theming, localization and routing.
Dan Abramov has devised some wise rules when to leave this gem hanging in the tree:
Now, probably you have followed this wise advice, but in the meantime, using libraries that use context, for example react-router, might still get you into trouble when combined with other libraries like react-redux or mobx-react, or even when combining with your own shouldComponentUpdate or the one provided by React.PureComponent. Long-standing issues can be found in in the React issue tracker and in the issue trackers of react-related libraries.
So, why is this blog relevant for you? Well either because
- You are a library author
- You use a library that uses context or you use context yourself, and you want to safely use shouldComponentUpdate (SCU) or implementations thereof (e.g. PureComponent, Redux connect, or MobX observer).
Why is Context + ShouldComponentUpdate problematic?
Context is used to communicate with deeply contained components. For example, a root component defines a theme, and any component in the component tree might (or might not) be interested in this information. Like in the official context example.
shouldComponentUpdate (SCU) on the other hand short circuits the re-rendering of a part of the component tree (including children), for example if the props or state of a component are not modified in a meaningful way. As far as the component can tell. But this might accidentally block context propagation…
Let’s demonstrate this interference issue in a simple app:
The problematic coordination between context and SCU is clearly visible once you press the “Red please!” button (on the “Result” tab above). The button itself gets a fresh color, but the todo items are not updated. The reason for this is simple: our TodoList component is smart; it knows that whenever it receives no new todo items, it doesn’t need to re-render. (The smartness is achieved by inheriting from PureComponent which implements shouldComponentUpdate).
However, due to this smartness (which is essential to keep React performant in big applications) the ThemedText components inside the TodoList don’t receive the new context with updated color! Because SCU returns false, neither the TodoList nor any of its descendants are updated.
Even worse, we cannot implement SCU in TodoList manually in such a way that this is fixed, because SCU will not receive the relevant context data (the color) as it isn’t (and shouldn’t!) be subscribed to this specific piece of context data. It isn’t a theme-aware component itself after all.
So to summarize, shouldComponentUpdate returning false causes any context update to be no longer propagated to child components. Pretty bad huh? Can we fix this?
ShouldComponentUpdate and Context can work together!
Did you notice that the problem only occurred once we updated the context? That is the crux to the solution of the problem as well. Just make sure that you never update the context. In other words:
- Context should not change; it should be (shallow) immutable
- Components should receive context only once; when they are constructed.
Or, to put it differently, we should not store state directly in our context. Instead, we should use context as a dependency injection system.
That means that SCU will not interfere anymore with context that needs to be passed on, because it never needs to pass a new context to its children. Awesome! That solves all our problems!
Communicating changes through context-based Dependency Injection
Except, ehh.. what if we want to change the color of our theme? Well simple, we have a dependency injection system (DI) in place, so we can pass down a store that manages our theme and subscribe to it. We never pass a new store around, but just make sure that the store itself is stateful and can notify components about changes:
Or, the complete runnable listing:
Note this example now reacts correctly to the color change. Yet, it still uses PureComponent. Also, the API of the important components App, TodoList and ThemedText didn’t change.
Our ThemeProvider implementation has become more complex though. It creates a Theme object which holds the state of our theme. Theme is also a (poor man’s) event emitter. This enables components like ThemedText to subscribe to future changes. The Theme object is passed through the component tree by ThemeProvider. Context is still used for that, but beyond the initial pass the context is not relevant anymore, as future updates are propagated by Theme itself, instead of by creating a new context.
This implementation is a bit over-simplified. A proper implementation would also need to clean up the event listeners in componentWillUnmount and should probably use setState instead of forceUpdate. But the good news is that this is purely a concern of the library you use / are building. It doesn’t affect the consumers of the library. An unexpected shouldComponentUpdate implementation in an intermediate component will no longer break the library.
By restraining the usage of context to be just a dependency injection system instead of a state container, we can make both context-based libraries and shouldComponentUpdate behave correctly without interference and without breaking the APIs of consumers. And, very important, it works within the current limitations of React’s context system. Just stick to this simple rule:
Context should be used as if it is received only once by each component.
A final reminder: Context is still an experimental feature, and you should avoid using context directly yourself (see Dan Abramov’s rules above). Instead use libraries that abstract over context (see below for some examples). But if you are a library author, or if you write nice higher-order components to deal with context, sticking to the above solution will avoid some nasty surprises.
Bonus: Using MobX observables as context simplifies things
(this section is mainly interesting if you are using or interested in MobX)
If you happen to use MobX, you can actually skip the whole event emitter stuff, and instead store (boxed) observables in the context and subscribe to them by using the observer decorator / higher-order component. This removes the need to manage the data subscriptions yourself:
Actually, one can make it even more simple by using the Provider / inject mechanism which is a tiny abstraction over React’s context mechanism, built into MobX. It removes the boilerplate of declaring contextTypes and similar things. Note that similar concepts can be found in generalized libs like recompose or react-tunnel.
For what it’s worth; note that although our initial DI-based solution was 1.5 times longer than the original codebase, this final solution is as long as the original problematic, implementation.