Explaining how to use the React Context API
Nowadays, writing web applications can get extremely tedious and complex. If you’re building a Web application especially using a component based UI framework such as React, you will sometimes send data from one component to another component. However, due to the complexity of your project, there might be times where you’ve had to send states from one component to another component that is not directly connected, thus further increasing the complexity. This is where React’s context API is incredibly useful. This new API solves one major problem–prop drilling. Even if you’re not familiar with the term, if you’ve worked on a React.js app, it has probably happened to you. Prop drilling is the processing of getting data from component A to component Z by passing it through multiple layers of intermediary React components.
What is the React context API?
According to React docs, “Context provides a way to pass data through the component tree without having to pass props down manually at every level.”
This means Context API helps us to skip the mandatory hierarchy of passing props for each component in its component tree.
Introducing the Context Web Store
First, we need to create the context, which we can later use to create providers and consumers. In a few words, the Context API allows you to have a central store where your data lives (yes, just like in Redux). To begin, we create a new Context. As we want the entire app to have access to this, we go to index.js
and wrap the app in ThemeContext.Provider
.
We also pass the value
prop to our Provider. This holds the data we want to save. For now, we just hardcode in 'Day'
.
import React from "react";
import ReactDOM from "react-dom";
import ThemeContext from "./themeContext";
import App from "./App";
ReactDOM.render(
<ThemeContext.Provider value={"Day"}>
<App />
</ThemeContext.Provider>,
document.getElementById("root")
);
Consuming Context with contextType
Currently, in App.js
, we are simply returning the <Image />
component.
import React from "react";
import Image from "./Image";
class App extends React.Component {
render() {
return (
<div className="app">
<Image />
</div>
);
}
}
export default App;
Our goal is to use Context to switch the classNames in Image.js
from Day
to Night
, depending on which image we want to render. To do this, we add a static property to our component called ContextType
and then use string interpolation to add it to the classNames in the <Image />
component.
Now, the classNames contain the string from the value
prop. Note: I have moved ThemeContext
into its own file to prevent a bug.
import React from "react";
import Button from "./Button";
import ThemeContext from "./themeContext";
class Image extends React.Component {
render() {
const theme = this.context;
return (
<div className={`${theme}-image image`}>
<div className={`${theme}-ball ball`} />
<Button />
</div>
);
}
}
Image.contextType = ThemeContext;
export default Image;
Context.Consumer
Unfortunately, this approach only works with class-based components. If you've learned about Hooks in React already, you'll know we can do just about anything with functional components these days. So for good measure, we should convert our components into functional components and then use ThemeContext.Consumer
component to pass info through the app.
This is done by wrapping our elements in an instance of <ThemeContext.Consumer>
and within that (where the children
go), providing a function which returns the elements. This uses the "render prop" pattern where we provide a regular function as a child that returns some JSX to render.
import React from "react";
import Button from "./Button";
import ThemeContext from "./themeContext";
function Image(props) {
// We don't need this anymore
// const theme = this.context
return (
<ThemeContext.Consumer>
{theme => (
<div className={`${theme}-image image`}>
<div className={`${theme}-ball ball`} />
<Button />
</div>
)}
</ThemeContext.Consumer>
);
}
// We don't need this anymore
// Image.contextType = ThemeContext;
export default Image;
Note: We also need to wrap the <Button />
component in <ThemeContext.Consumer>
- this allows us to add functionality to the button later.
import React from "react";
import ThemeContext from "./themeContext";
function Button(props) {
return (
<ThemeContext.Consumer>
{context => (
<button className="button">
Switch
<span role="img" aria-label="sun">
?
</span>
<span role="img" aria-label="moon">
?
</span>
</button>
)}
</ThemeContext.Consumer>
);
}
export default Button;
Extract Context Provider
We are currently passing a hard-coded value down through the Provider, however, our goal is to switch between night and day with our button.
This requires moving our Provider to a separate file and putting it in its own component, in this case, called ThemeContextProvider
.
import React, { Component } from "react";
const { Provider, Consumer } = React.createContext();
class ThemeContextProvider extends Component {
render() {
return <Provider value={"Day"}>{this.props.children}</Provider>;
}
}
export { ThemeContextProvider, Consumer as ThemeContextConsumer };
Note: the value property is now being handled in the new file ThemeContext.js, and should, therefore, be removed from index.js.
Changing Context
To wire up the button, we first add state to ThemeContextProvider
:
import React, { Component } from "react";
const { Provider, Consumer } = React.createContext();
// Note: You could also use hooks to provide state and convert this into a functional component.
class ThemeContextProvider extends Component {
state = {
theme: "Day"
};
render() {
return <Provider value={"Day"}>{this.props.children}</Provider>;
}
}
export { ThemeContextProvider, Consumer as ThemeContextConsumer };
Next, we add a method for switching between day and night:
toggleTheme = () => {
this.setState(prevState => {
return {
theme: prevState.theme === "Day" ? "Night" : "Day"
};
});
};
Now we change our value
property to this.state.theme
so that it returns the info from state.
render() {
return <Provider value={this.state.theme}>{this.props.children}</Provider>;
}
}
Next, we change value
to an object containing {theme: this.state.theme, toggleTheme: this.toggleTheme}
, and update all the places where we use a single value to look for theme
in an object. This means that every theme
becomes context
and every reference to theme
as value becomes context.theme
.
Finally, we tell the button to listen for the onClick
event and then fire context.toggleTheme
- this updates the Consumers which are using the state from the Provider. The code for the button looks like this:
import React from "react";
import { ThemeContextConsumer } from "./themeContext";
function Button(props) {
return (
<ThemeContextConsumer>
{context => (
<button onClick={context.toggleTheme} className="button">
Switch
<span role="img" aria-label="sun">
?
</span>
<span role="img" aria-label="moon">
?
</span>
</button>
)}
</ThemeContextConsumer>
);
}
export default Button;
Our button now switches the image between night and day in one click!
Context caveats
Like all good things in code, there are some caveats to using Context:
Don't use Context to avoid drilling props down just one or two layers. Context is great for managing state which is needed by large portions of an application. However, prop drilling is faster if you are just passing info down a couple of layers.
Avoid using Context to save state that should be kept locally. So if you need to save a user's form inputs, for example, use local state and not Context.
Always wrap the Provider around the lowest possible common parent in the tree - not the app's highest-level component. No need for overkill.
Lastly, if you pass an object as your value prop, monitor performance and refactor as necessary. This probably won't be needed unless a drop in performance is noticeable.
Wrap up
This example is pretty simple and it would probably be easier to put state in the app and pass it down via props. However, it hopefully shows the power of having Consumers which can access data independently of the components above them in the tree.