Author avatar

Chris Dobson

Using the React Context API

Chris Dobson

  • May 7, 2019
  • 12 Min read
  • 722 Views
  • May 7, 2019
  • 12 Min read
  • 722 Views
Web Development
React

Introduction

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.

The React Context API

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.

Example

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.

App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const darkTheme = {
  name: "dark",
  backgroundColor: "#282c34",
  color: "white",
  linkColor: "#61dafb",
  logoBackground: "white",
};
const lightTheme = {
  name: "light",
  backgroundColor: "white",
  color: "#282c34",
  linkColor: "#61dafb",
  logoBackground: "red",
};
const availableThemes = [darkTheme, lightTheme];
const App = () => {
  const [selectedTheme, setSelectedTheme] = React.useState(darkTheme);
  const selectTheme = name => {
    const theme = availableThemes.find(theme => theme.name === name);
    if (theme) {
      setSelectedTheme(theme);
    }
  };
  return (
    <div className="app" style={{ backgroundColor: selectedTheme.backgroundColor, color: selectedTheme.color }}>
      <Header
        selectedTheme={selectedTheme}
        availableThemes={availableThemes.map(theme => theme.name)}
        selectTheme={selectTheme}
      />
      <Body selectedTheme={selectedTheme} />
      <Footer selectedTheme={selectedTheme} />
    </div>);
};
javascript

Header

1
2
3
4
5
6
7
8
9
10
11
12
13
const Header = props => (
  <header className="app-header" style={{ backgroundColor:  props.selectedTheme.backgroundColor, color:  props.selectedTheme.color  }}>
    <span>{`Current theme: ${props.selectedTheme.name}`}</span>
    <div className="app-menu">
      {props.availableThemes.map(theme => (
        <ThemeItem
          key={theme}
          selectedTheme={props.selectedTheme}
          themeName={theme}
          select={() => props.selectTheme(theme)}
        />))}
    </div>
  </header>);
javascript

ThemeItem

1
2
3
4
const ThemeItem = props => (
    <button style={{ color: props.selectedTheme.linkColor }} onClick={props.select}>
        {props.themeName}
    </button>);
javascript

Body

1
2
3
4
const Body = props => (
    <div className="app-body">
        <Logo logoBackground={props.selectedTheme.logoBackground} />
    </div>);
javascript

Footer

1
2
3
4
const Footer = props => (
  <footer className="app-footer">
    <span>{props.selectedTheme.name}</span>
  </footer>);
javascript

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.

Creating a 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:

1
2
3
4
5
createContext({
    selectedTheme: {},
    availableThemes: [],
    selectTheme: theme => {},
});
javascript

Providing Context

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const availableThemes = [darkTheme, lightTheme];

const ThemeProvider = props => {
  const [selectedTheme, setSelectedTheme] = React.useState(darkTheme);
  const selectTheme = name => {
    const theme = availableThemes.find(theme => theme.name === name);
    if (theme) {
      setSelectedTheme(theme);
    }
  };
  return (
    <themeContext.Provider value={{ selectedTheme, selectTheme, availableThemes }}>
      {props.children}
    </themeContext.Provider>);
};
javascript

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.

Consuming Context

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:

1
const { selectedTheme, availableThemes, selectTheme } = React.useContext(themeContext);
javascript

The above uses javascript object destructuting but this could also be used:

1
const theme = useContext(themeContext)
javascript

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:

1
2
3
4
5
6
7
8
9
class App extends React.Component {
  render() {
    return (
      <div className="app" style={{ backgroundColor: this.context.selectedTheme.backgroundColor, color: this.context.selectedTheme.color }}>
        ...
      </div>);
    }
}
App.contextType = themeContext;
javascript

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:

1
2
3
4
5
6
7
8
const App = () => (
  <themeContext.Consumer>
    {theme => (
      <div className="app" style={{ backgroundColor: theme.selectedTheme.backgroundColor, color: theme.selectedTheme.color }}>
	    ...
      </div>
    )}
  </themeContext.Consumer>);
javascript

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:

App

1
2
3
4
5
6
7
8
9
const App = () => {
  const { selectedTheme } = React.useContext(themeContext);
  return (
    <div className="app" style={{ backgroundColor: selectedTheme.backgroundColor, color: selectedTheme.color }}>
      <Header />
      <Body />
      <Footer />
    </div>);
};
javascript

Header

1
2
3
4
5
6
7
8
9
10
11
12
const Header = () => {
  const { selectedTheme, availableThemes } = React.useContext(contextTheme);
  return (
    <header className="app-header" style={{ backgroundColor: selectedTheme.backgroundColor, color: selectedTheme.color }}>
      <span>{`Current theme: ${selectedTheme.name}`}</span>
      <div className="app-menu">
        {availableThemes.map(theme => (
          <ThemeItem key={theme} themeName={theme.name} />
        ))}
      </div>
    </header>);
};
javascript

ThemeItem

1
2
3
4
5
6
7
const ThemeItem = props => {
  const { selectedTheme, selectTheme } = React.useContext(themeContext);
  return (
    <button style={{ color: selectedTheme.linkColor }} onClick={() => selectTheme(props.themeName)}>
      {props.themeName}
    </button>);
};
javascript

Body

1
2
3
4
5
6
7
const Body = () => {
  const { selectedTheme } = React.useContext(themeContext);
  return (
    <div className="app-body">
      <Logo logoBackground={selectedTheme.logoBackground} />
    </div>);
};
javascript

Footer

1
2
3
4
5
6
7
const Footer = () => {
  const { selectedTheme } = React.useContext(themeContext);
  return (
    <footer className="app-footer">
      <span>{selectedTheme.name}</span>
    </footer>);
};
javascript

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.

Composition

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
2
3
4
5
<ThemeProvider>
  <CustomerProvider>
    ...
  </CustomerProvider>
</ThemeProvider>
javascript

Sample

A sample application using the context API can be found here.

5