Cover image by Rebecca Jackson on Flickr.
What
First, what is a context?
A context is a way to store and pass down data a React element tree without writing it into a prop every layer of the hierarchy. It consists of two components, a Provider
and a Consumer
.
A Provider
stores some data that can be accessed via the Consumer
somewhere in the tree below the Provider
. So the Consumer
has to be a descendant of the Provider
.
A Consumer
accesses the Provider
data and makes it available to its children
via a render prop. The nice thing being, a Consumer
doesn't have to be a direct child of a Provider
it can be anywhere in the tree below it.
Why
Sometimes you use data inside your application that is quasi global. Sometimes it's global to the whole application, sometimes just global to a screen or page, but it is used in many places.
For example, you want to use theme information in all of your UI components, or you want to make the data of the currently logged in user available to many components, or you have an API client that needs to be configured one time and than used all over your application.
Now you could make this data simply global, but this would get unwieldy rather quick. A context is a way to do this in an encapsuled way and since none of the elements between the Provider
and the Consumer
know about the context or its data, it also is another way to add dependency injection into your app and make it more resilient to change.
How
So how do you create a context? And how do you use it later?
The context API got rewritten not long ago for flexibility and ease of use. React provides a simple function to create a context.
const Context = React.createContext();
This function returns an object with two attributes Provider
and Consumer
that contain the components which are needed to use this context later.
A basic usage could look like this:
<Context.Provider value="context data">
...
<Context.Consumer>
{value => <p>{value}</p>}
</Context.Consumer>
...
</Context.Provider>
The Provider
takes a value
prop that becomes its state
. The Consumer
takes a render
prop in form of children as a function. This function receives the current value
as argument.
Often you have more complex data and a way to change this data down in the components that use it.
Here a more complex example:
const Context = React.createContext();
class A extends React.Component {
state = { x: 1 };
handleContextChange = x => this.setState({ x });
render() {
const contextValue = {
data: this.state,
handleChange: this.handleContextChange
};
return (
<Context.Provider value={contextValue}>
<B />
</Context.Provider>
);
}
}
const B = props => <div><C /></div>;
const C = props => (
<Context.Consumer>
{({ handleChange, data }) => (
<div>
<button onClick={() => handleChange(2)}>Change</button>
<D text={data.x} />
</div>
)}
</Context.Consumer>
);
const D = props => <p>{props.text}</p>;
We start by creating a Context
.
Then we use it in component A which is the top of our hierarchy. The value for our Context.Provider
is the state of A
and a method of A
that handles changes to this state
. When the state
changes, the Context.Provider
gets a new value. A
is also the storage of our state, the context just pipes it down the hierarchy.
In component C
we use the Context.Consumer
, it receives a function via its children
render prop. If the value of the Context.Provider
changes this function is simply called again and renders with the new value.
As you can see, component B
which is between A
and C
is completely elusive to the whole context arrangement. It just renders component C
indifferent about its implementation.
Also, component D
and the button
element don't know anything about context. They just get data and the change handler function passed via their props and can use them as any other prop. D
wants the text it renders passed into its text
prop instead of children and button is just a regular old button that executes everything passed into its onClick
prop. So context is an extension of the dependency injection used in pure render props.
Because the pair of Provider
and Consumer
are created per createContext()
call, you can even have multiple contexts. Every context is encapsuled and safe from actions of other contexts.
Conclusion
The new context API is much more flexible than the old one and works without prop-types and since it's now stable, you can finally use it without fear that it goes away soon.
It also extends the dependency injection concepts used in render props by letting you pass down state from a component to a deep ancestor of it without telling the in-betweens anything of it.