How to handle React context in a reliable way.
Context — no pun intended —
One-way data flow is one of the most compelling traits of React. It makes your app easier to reason about. You can figure out exactly what props are being passed just by looking at your component. You can even get more out of it adopting the container vs presentational component pattern.
But, if you’ve used React for a while, chances are that you’ve found a situation where you want to pass data through the component tree without having to pass the props down at every level.
React has a feature to solve this problem: context.
Probably you’re getting profit of it even without realize it. Due to some of the most used libraries in the React ecosystem take advantage of it: React-Router, React-Redux, and many more.
But, it comes with a price and you have to be careful. React’s documentation warn us about it:
This post is intended to show you how to use React context reliably. Outlining the most common pitfalls and how to avoid them.
How does React context work?
First you need a context provider component. Which is no more than a normal component that requires childContextTypes and getChildContext:
- childContextTypes: static property that allows you to declare the structure of the context object being passed to your component’s descendants. Similar fashion to propTypes but not optional.
- getChildContext: prototype method that returns the context object to pass down the component’s hierarchy. Every time the state changes or the component receives new props this method will be called.
Let’s see how is done:
class ContextProvider extends React.Component {
static childContextTypes = {
currentUser: React.PropTypes.object
}; getChildContext() {
return {currentUser: this.props.currentUser};
}
render() {
return(...);
}
}
Any descendant of this component can access the context as follows:
/* ES6 class component */
class ContextConsumer extends React.Component {
/*
contexTypes is a required static property to declare
what you want from the context
*/ static contextTypes = {
currentUser: React.PropTypes.object
}; render() {
const {currentUser} = this.context;
return <div>{currentUser.name}</div>;
}
}/* ES6 class component with an overwritten constructor */
class ContextConsumer extends React.Component {
static contextTypes = {
currentUser: React.PropTypes.object
}; constructor(props, context) {
super(props, context);
...
} render() {
const {currentUser} = this.context;
return <div>{currentUser.name}</div>;
}
}/* Functional stateless component */
const ContextConsumer = (props, context) => (
<div>{context.currentUser.name}</div>
);
ContextConsumer.contextTypes = {
currentUser: React.PropTypes.object
};
The context consumer components need to declare what they want from the context via contextTypes. Then, they can access the context via this.context in the case of the ES6 class components. Instead, when your consumer is a functional stateless component you can access the context through the second parameter of the function.
Also, you can reference context in some of the component’s lifecycle methods:
componentWillReceiveProps(nextProps, nextContext) {...}shouldComponentUpdate(nextProps, nextState, nextContext) {...}componentWillUpdate(nextProps, nextState, nextContext) {...}componentDidUpdate(previousProps, previousContext) {...}
Which are the drawbacks?
- As we referenced above, React context is experimental, the API is likely to change in the future. This makes the code that uses it overly brittle. Any API change will break every context provider and consumer. Leading to a maintenance nightmare.
- Also a React context is basically a global variable in the scope of a single React subtree. This makes your components more coupled, and nearly impossible to reuse outside the aforementioned subtree.
- Moreover, if a context value provided by a component changes, descendants that use that value won’t update if an intermediate parent returns false from shouldComponentUpdate:
Woah! There is a lot to take into account… and a question arises:
Should I use React unstable context feature?
Dan Abramov nailed it!
Let’s see how using higher order components (HOCs for brevity) you can mitigate some of the inherent hassle of using React context.
HOCs to the rescue
A HOC is a function that accepts a base React component and returns a new component with additional functionality. Basically a HOC enhances a component with new capabilities. This way you can abstract common behaviors into reusable pieces.
More about HOCs:
Mixins Are Dead. Long Live Composition
Providing HOCs you can easily solve the code fragility problem of the React context. You’ll only need to update one place if the API changes. Let’s illustrate a possible implementation:
const provideContext =
(childContextTypes, getChildContext) => (Component) => {
class ContextProvider extends React.Component {
static childContextTypes = childContextTypes;
getChildContext = () => getChildContext(this.props);
render() {
return <Component {...this.props} />;
}
} return ContextProvider;
};const consumeContext = (contextTypes) => (Component) => {
/* The context is passed as props. This way the component is
completely decoupled from the context API.
*/
const ContextConsumer = (props, context) =>
<Component {...props} {...context} />;
ContextConsumer.contextTypes = contextTypes; return ContextConsumer;
};
Then, you can use it as follows:
const Child = ({color}) => (
<div style={{backgroundColor: color}}>
Hello context!!!
</div>
);
const ChildwithContext = consumeContext({
color: React.PropTypes.string
})(Child);const MiddleComponent = () => <ChildwithContext />;const App = provideContext(
{color: React.PropTypes.string},
() => ({color: 'red'})
)(MiddleComponent);
Another alternative is to use the amazing library Recompose:
Recompose provides a bunch of very useful HOCs to suit all your needs. If you embrace Recompose you’ll change the way you write React applications. It provides withContext and getContext as alternatives to our provideContext and consumeContext respectively.
As an advantage you no longer have to maintain the HOCs. But, if your application is small or you only need these two functions, relying on Recompose might be overkill.
Don’t use context to pass your model data through components
HOCs give you a lot, but not enough… It’s up to you to choose what data to pass via context.
If you pass model data you’ll only get more troubles:
- Makes your app harder to reason about.
- Couples your components.
Use context for real globals, not just for save typing. The current logged-in user, theme info, internationalization and localization are all good examples.
A working example please!
Conclusions
In software development like in life in general, it’s all about trade-offs. And… albeit unstable, React context is a powerful tool at your disposal. But, you know the old saying: with great power comes great responsibility.
So, use context in a reliable way. HOCs aren’t the panacea but help a lot in these matters.
I hope this post has gave you a better perspective and clarified a little bit the React context feature.
Huge thanks to Dan Abramov for reviewing this post. I really appreciate his contributions to the community.