The old vs new React Context API
With React 16.3 a new Context API will be introduced, making the old, experimental Context API obsolete. This marks a good time to take a look at the old and the new, what changes and why context is even needed in the first place.
What is Context
?
I’m sure you’ve had the problem of a component needing some data from way up the component tree. Without using a state-management library like Redux the only solution seems to be prop drilling,passing the data down each component in-between the data provider and the data receiver, which looks something like this:
<Provider {...props}>
<ComponentI {...props}>
<ComponentDont {...props}>
<ComponentNeed {...props}>
<ComponentThis {...props}>
<Receiver {...props}>
This sucks.
React provided a experimental way to solve this problem with the context API
. Let’s see how it works.
The old context API
The idea is for the Provider to define a plain object from which the Receiver can select properties they are interested in.
Provider API
Let’s say we have a color
that we want to pass down. First we define a getChildContext()
function that returns the plain object containing the data we want to pass down.
Then we define childContextTypes
, like we would define prop types. This is what it looks like:
class MyProvider extends React.Component {
getChildContext() {
return { color: "green" }
}
render() {
return /* ... */
}
}
MyProvider.childContextTypes = {
color: PropTypes.string,
}
Receiver API
The Receiver selects which of the properties of the context object he wants with the contextTypes
property, analogously to the childContextTypes
property of the Provider. Then these properties are made available on the class’ context:
class MyReceiver extends React.Component {
render() {
const { color } = this.context
return `The color is ${color}`
}
}
MyReceiver.contextTypes = {
color: PropTypes.string,
}
Usage examples
Even though the React team was quite clear that it’s not a stable API, many libaries made use of it. Two quick examples:
react-router
React-Router makes the router
property available in its context. See this short code snippet from the react-router source code:
class Router extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
children: PropTypes.node
};
static contextTypes = {
router: PropTypes.object
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
/* ... */
react-redux
React-redux makes the redux store available to react components. For this it uses the Provider
component as the Parent on the one hand as well as the connect
function that wraps components in a Higher-Order Component with the redux state made available to Child components. See these code snippets from the react-redux library source code:
inside Provider.js:
export function createProvider(storeKey = "store", subKey) {
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
/* ... */
render() {
return Children.only(this.props.children)
}
}
/* ... */
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
[storeKey]: storeShape.isRequired,
[subscriptionKey]: subscriptionShape,
}
return Provider
}
inside connectAdvanced.js:
/* ... */
const contextTypes = {
[storeKey]: storeShape,
[subscriptionKey]: subscriptionShape,
}
const childContextTypes = {
[subscriptionKey]: subscriptionShape,
}
/* ... */
getChildContext() {
const subscription = this.propsMode ? null : this.subscription
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
/* ... */
The new API
Things are going to be quite a bit simpler. Let’s look at an example:
An Example
const BananaContext = React.createContext("hello")
class App extends React.Component {
render() {
return (
<BananaContext.Provider value="hola">
<BananaContext.Consumer>
{context => <div>{context}</div>}
</BananaContext.Consumer>
</BananaContext.Provider>
)
}
}
// Prints hola
Let’s look at the individual parts.
React.createContext()
React.createContext()
creates an object that exposes two objects: A Provider
and a Consumer
. It takes a default argument that will get used if the Consumer
is not wrapped in a corresponding Provider
. Like this:
const BananaContext = React.createContext("hello")
class App extends React.Component {
render() {
return (
<BananaContext.Consumer>
{context => <div>{context}</div>}
</BananaContext.Consumer>
)
}
}
// Prints hello
The default argument can of course also be an object if you need to provide more than one value.
Provider
The Provider
provides the value in its appropriately name value
prop. A more interesting value would be something from its internal state like so: value={this.state.someValue}
.
Consumer
The Consumer
takes a function as a child with the context
provided as its only parameter. The function as a child pattern makes it flexible in the way how to process the given context. I’ll definitely write a post about this pattern since it’s pretty awesome.
Conclusion
The new context API is definitely easier to understand and reason about. It’s finally a first class citizen in the React world and I’m looking forward to use it in my projects.