Introducing the React Context API
The new React Context API is touted (at least on Twitter and a number of articles) as solving the need to use a state management tool, when I think in reality what it solves is easy dependency injection: Take something that lives at the top of your app and directly inject it into a lower level component without having to pass it all the way down.
The code used in the Gallery example further down can be found here. Credit to Marian Serna for creating the code which this example is based off of.
Michel Weststrate, the creator of MobX says:
context is not a state management mechanism but more DI. It didn't change anything in term of what is possible, just cleaned and finally formalized the api. So imho the answer to that question is the same as in React 16.2
— Michel Weststrate (@mweststrate) April 4, 2018
What it solves
Let's start with a scenario fairly common in React, where you have your state living at the top level of your app. Let's look at a simple component tree below:
- App- Header- HeaderUser- Main- UserSettings- SettingsForm
In this scenario we'd most likely have our state live in the App
component and we'd have to pass down the user's information 2 levels to display it in the HeaderUser
component and 3 levels to display it in SettingsForm
. Now let's say SettingsForm
wants to update the state, you'd have to pass a function from App -> Main -> UserSettings -> SettingsForm
... the worst!
The most common thing at this point would be to reach for Redux or MobX and extract the state out of your component tree, having it live with the state management's store.
With the new React context API, you may think before immediately reaching for an external state management library. It essentially gives you an easy way to have state live at the top level in your component tree (App
in this case) but "inject" it as a prop in a lower level component in the state tree. In this case we can inject the user's data directly into HeaderUser
and SettingsForm
, and also inject a function that modifies the state directly into SettingsForm
without having to pass it down the entire tree.
Context Overview
The new context API consists essentially of 3 things: Context, Provider, and Consumer. Context is somewhat of a "container" that you create which allows you to define the Provider and the Consumer of that context.
A Provider is a concept that let's you say: Here is a value which I'm defining (or providing, hence the name) up here at the top of my component tree, expecting that it will be used (or consumed in other words) lower down in the tree.
A Consumer works hand-in-hand with the Context's Provider, essentially allowing you to reach into your Context, and easily inject the Context's value into a component, skipping many levels in the component tree.
In practical terms, you can directly inject the user's data into the HeaderUser
component, or directly into the SettingsForm
component, without having to pass it all the way down.
Gallery
We'll now start to look at a simple React gallery which searches and displays images from the unsplash API. The component tree looks like:
- App- Form- Status- Images- Image
What we'll end up with is this, which looks sort of intense, but we're just injecting something from the provider into our component using a consumer.
- GalleryProvider- GalleryConsumer- App- GalleryConsumer- Form- GalleryConsumer- Status- GalleryConsumer- Images- Image
Context
To use the new Context API we first have to create a Context, using the React.createContext()
function. From the Context we can access its Provider and Consumer.
export const GalleryContext = React.createContext();
Provider
To provide a bite-size example of the provider (which be expanded to a real example below), we'll define a GalleryProvider functional component, which renders the props.children
inside of the GalleryContext.Provider
component. So what's happening? By doing this, we can access the provider's value "Hot stuff" through the Consumer at any level lower in the component tree.
const GalleryProvider = props => (<GalleryContext.Provider value="Hot stuff">{props.children}</GalleryContext.Provider>);
A pattern I've been following is to define a component named GalleryProvider
which encapsulates the Context's state, and provides the `GalleryContext.
export class GalleryProvider extends React.Component {state = {term: "",images: [],status: "initial"};fetchImages = async term => {this.setState({ term });const response = await axios.get("https://api.unsplash.com/search/photos");this.setState({status: "done",images: response.data.results});};render() {return (<GalleryContext.Providervalue={{ ...this.state, fetchImages: this.fetchImages }}>{this.props.children}</GalleryContext.Provider>);}}
Our value
here is an object which consists of all the properties from the state plus a function allowing components at a lower level to fetch images from the API, and therefore modify the state.
To use this provider, you simply wrap it around your top level component.
ReactDOM.render(<GalleryProvider><App /></GalleryProvider>,document.getElementById("root"));
Consumer
Now that we've set up our Context and have created the Provider which wraps our app at the top level, we can now inject its value
into any of our lower level components. Let's say we want to inject the fetchImages
function into our Form component. We'd start by declaring the <GalleryContext.Consumer>
component. It's child is a function which receives the value
which was given to <GalleryContext.Provider>
inside of the GalleryProvider
component. This function must return what it wants to render, which in this case is the <Form />
component.
<GalleryContext.Consumer>{({ fetchImages }) => <Form fetchImages={fetchImages} />}</GalleryContext.Consumer>
Refactoring to separate component + "connected component"
When thinking of how to organize an app which uses React Context, I wanted to avoid having the Consumer code in the same file as the component which it "wraps"... the reason for this is because it makes it difficult to test the component in isolation.
Let's look at how we might organize the Form component:
- components- Form- Form.js- index.js- ... etc
In the index.js
file we can have the wrapped version of our Form component.
import React from "react";import { GalleryContext } from "../../contexts/GalleryContext";import Form from "./Form";export default props => (<GalleryContext.Consumer>{({ fetchImages }) => <Form {...props} fetchImages={fetchImages} />}</GalleryContext.Consumer>);
What we have done here is sort of mimic what happens when you use @inject('Store')
in MobX or connect()
with Redux. We've created a higher-order component, which translates to a function which takes in the props coming from its parent and returns an enhanced Form component, which now receives all incoming props plus the fetchImages
function given to use through the consumer.
Watch out for this gotcha
For a while I was trying the code below, which was failing horribly. It was incorrect for 2 reasons... it ends up exporting an object, not a class or a function which React expects. Plus it provides no way to pass props from the parent into the Form component. You need that function (props) => { return consumer }
shown above.
import React from "react";import { GalleryContext } from "../../contexts/GalleryContext";import Form from "./Form";export default (<GalleryContext.Consumer>{({ fetchImages }) => <Form {...props} fetchImages={fetchImages} />}</GalleryContext.Consumer>);
Testing
Because we separated the actual Form component from the higher-order component which injects our context's value, we can easily test it just by passing the props that it expects to receive.
import Form from "./Form";it("triggers calls fetchImages on form submission", () => {const spy = sinon.spy();const wrapper = mount(<Form fetchImages={spy} />);wrapper.find("form").first().simulate("submit");expect(spy.calledOnce).toBe(true);});
Conclusion
I think I'd still have to recommend against reaching for the Context API right away. My rules to follow would be:
- Stick with local component state when it is something confined to that component itself or maybe 1 level below.
- When things get larger, switch to using MobX or Redux, but if you're writing a library which needs dependency injection, like MobX and Redux both do, by all means use the context API.
In my opinion for the average app it doesn't really provide an improvement for the typical React app already happily using a state management library. But hey, it's good to know which tools you have in the toolbox.