A React application will often require some state to be available in multiple components. There are a number of ways to achieve this; for instance, we could keep the state in a top-level component and pass it down in props into every child that needs it or we could use a state management library, such as Redux or MobX, that, among many other things, provides an API to inject states into components.
Passing the state through props is a perfectly good approach to this and will incur no performance penalty; the only real issue is that, in a large component tree, passing the state through many different levels (many where it may not be used) can clutter the component props and make it a little more difficult to read the code. Also, if a component at the bottom of a tree needs some state that is currently not being passed then, what ought to be a relatively small change could end up impacting a number of different components for no other reason than to get the state down to the component that requires it. In many cases, this may well be the preferred approach as it makes component re-use simpler but, on some occasions, another approach may be better.
Using a state management library is, also, a perfectly good approach to this problem and one that is used in many, many applications. However, this would add another dependency to the application which could be avoided by using functionality that is built in to React. The API that is built in to React is by no means a full replacement for state management libraries but, in some cases, will suffice.
React's Context API can be used to provide state across all, or part, of a React application. As this guide will show, this is done using the createContext
function to define the context, a Provider
component in which to wrap the component tree that requires access to the context. Then, either a Consumer
component, a context
class member, or the useContext
hook can be used within a component to access the state.
A good example of where the React Context may be a good approach is when using a theme for components. Different parts of the theme will be required by nearly all components and an option to change the current theme will be needed.
These components give an idea of how this might be implemented when passing the theme as props. In order to focus on how to create and use a React Context, this example will be fairly simple and use a small component tree but it should be fairly easy to imagine how this would look in a deeply nested tree.
1const darkTheme = {
2 name: "dark",
3 backgroundColor: "#282c34",
4 color: "white",
5 linkColor: "#61dafb",
6 logoBackground: "white",
7};
8const lightTheme = {
9 name: "light",
10 backgroundColor: "white",
11 color: "#282c34",
12 linkColor: "#61dafb",
13 logoBackground: "red",
14};
15const availableThemes = [darkTheme, lightTheme];
16const App = () => {
17 const [selectedTheme, setSelectedTheme] = React.useState(darkTheme);
18 const selectTheme = name => {
19 const theme = availableThemes.find(theme => theme.name === name);
20 if (theme) {
21 setSelectedTheme(theme);
22 }
23 };
24 return (
25 <div className="app" style={{ backgroundColor: selectedTheme.backgroundColor, color: selectedTheme.color }}>
26 <Header
27 selectedTheme={selectedTheme}
28 availableThemes={availableThemes.map(theme => theme.name)}
29 selectTheme={selectTheme}
30 />
31 <Body selectedTheme={selectedTheme} />
32 <Footer selectedTheme={selectedTheme} />
33 </div>);
34};
1const Header = props => (
2 <header className="app-header" style={{ backgroundColor: props.selectedTheme.backgroundColor, color: props.selectedTheme.color }}>
3 <span>{`Current theme: ${props.selectedTheme.name}`}</span>
4 <div className="app-menu">
5 {props.availableThemes.map(theme => (
6 <ThemeItem
7 key={theme}
8 selectedTheme={props.selectedTheme}
9 themeName={theme}
10 select={() => props.selectTheme(theme)}
11 />))}
12 </div>
13 </header>);
1const ThemeItem = props => (
2 <button style={{ color: props.selectedTheme.linkColor }} onClick={props.select}>
3 {props.themeName}
4 </button>);
1const Body = props => (
2 <div className="app-body">
3 <Logo logoBackground={props.selectedTheme.logoBackground} />
4 </div>);
1const Footer = props => (
2 <footer className="app-footer">
3 <span>{props.selectedTheme.name}</span>
4 </footer>);
In the examples in this guide, state is handled using the hooks API. This could also be achieved using class components.
In the code above, the App component (at the top of the tree) stores a list of available themes, and the current theme in its state. It then passes these, along with a function to change the current theme, to its children, which then use these props and/or pass them to their children. This could be simplified somewhat by using context.
A React context is created by using the createContext function. This function takes a default implementation as a parameter and returns an object containing Provider
and Consumer
members that can be used to provide context to components and access that context from a component.
This code will create the context for the theme example:
1createContext({
2 selectedTheme: {},
3 availableThemes: [],
4 selectTheme: theme => {},
5});
As discussed above, the call to createContext
returns an object with a Provider
component as a member. This provider component takes a prop, value
, that will be provided as the context. This component can then be used inside a stateful component to provide everything that is currently implemented in the top-level component and passed around as props, like this:
1const availableThemes = [darkTheme, lightTheme];
2
3const ThemeProvider = props => {
4 const [selectedTheme, setSelectedTheme] = React.useState(darkTheme);
5 const selectTheme = name => {
6 const theme = availableThemes.find(theme => theme.name === name);
7 if (theme) {
8 setSelectedTheme(theme);
9 }
10 };
11 return (
12 <themeContext.Provider value={{ selectedTheme, selectTheme, availableThemes }}>
13 {props.children}
14 </themeContext.Provider>);
15};
Unsurprisingly, this component is very similar to the original page component, the main difference being that it renders the children
prop. This means that it can be used to wrap any component. Any component inside the tree wrapped with this component will now have access to the context provided by it.
There are three ways to consume the context from a provider, using either the hooks API, the context
member of a class component, or the Consumer
component.
Using the hooks API is done using the useContext
function which will return the value
from the provider as an object. The code to do this looks like this:
1const { selectedTheme, availableThemes, selectTheme } = React.useContext(themeContext);
The above uses javascript object destructuting but this could also be used:
1const theme = useContext(themeContext)
The theme can then be used anywhere in the component.
Using the context
member in a class first requires the static contextType
field to be set and then the context object is available in the this.context
member. The code looks like this:
1class App extends React.Component {
2 render() {
3 return (
4 <div className="app" style={{ backgroundColor: this.context.selectedTheme.backgroundColor, color: this.context.selectedTheme.color }}>
5 ...
6 </div>);
7 }
8}
9App.contextType = themeContext;
Using the Consumer
member of the context object allows the context to be consumed within a functional, stateless, component and the code looks like this:
1const App = () => (
2 <themeContext.Consumer>
3 {theme => (
4 <div className="app" style={{ backgroundColor: theme.selectedTheme.backgroundColor, color: theme.selectedTheme.color }}>
5 ...
6 </div>
7 )}
8 </themeContext.Consumer>);
The context value can then be used anywhere inside the Consumer
component. This approach can also be used inside a class component rather then the context
class member if preferred.
The original components will now look like this, using hooks:
1const App = () => {
2 const { selectedTheme } = React.useContext(themeContext);
3 return (
4 <div className="app" style={{ backgroundColor: selectedTheme.backgroundColor, color: selectedTheme.color }}>
5 <Header />
6 <Body />
7 <Footer />
8 </div>);
9};
1const Header = () => {
2 const { selectedTheme, availableThemes } = React.useContext(contextTheme);
3 return (
4 <header className="app-header" style={{ backgroundColor: selectedTheme.backgroundColor, color: selectedTheme.color }}>
5 <span>{`Current theme: ${selectedTheme.name}`}</span>
6 <div className="app-menu">
7 {availableThemes.map(theme => (
8 <ThemeItem key={theme} themeName={theme.name} />
9 ))}
10 </div>
11 </header>);
12};
1const ThemeItem = props => {
2 const { selectedTheme, selectTheme } = React.useContext(themeContext);
3 return (
4 <button style={{ color: selectedTheme.linkColor }} onClick={() => selectTheme(props.themeName)}>
5 {props.themeName}
6 </button>);
7};
1const Body = () => {
2 const { selectedTheme } = React.useContext(themeContext);
3 return (
4 <div className="app-body">
5 <Logo logoBackground={selectedTheme.logoBackground} />
6 </div>);
7};
1const Footer = () => {
2 const { selectedTheme } = React.useContext(themeContext);
3 return (
4 <footer className="app-footer">
5 <span>{selectedTheme.name}</span>
6 </footer>);
7};
It should now, hopefully, be relatively simple to convert this to using the Consumer
component or the class component.
If any of these approaches for consuming state are used inside a component tree that is not wrapped in the relevant Provider
, the default context will be used.
It can be tempting, when using context, to create a single global context containing all states that may be needed across components. However, an application can have any number of different contexts, so the code would be much simpler if you choose to separate the code into different contexts. For instance, if there was a requirement for a customer context as well as the theme context this code could be used to compose them:
1<ThemeProvider>
2 <CustomerProvider>
3 ...
4 </CustomerProvider>
5</ThemeProvider>
A sample application using the context API can be found here.