A closer look at React’s Context API

Mar 05, 2020

TL:DR; Context gives us a way of sharing ‘global’ data with many components at different nesting levels.

For someone who has worked with React for over 4 years, I must confess that my understanding of the Context API is not as solid as it perhaps should be.

Having also recently rediscovered that teaching or writing is the best way to really learn a concept, I decided to write this wonderful post!

What the heck is Context?

The official React documentation describes Context as follows:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

In other words, Context provides us an easier way of passing data down to deeply nested components without having to pass it down at each level of the component tree.

To better understand Context, let’s take a look at an example of the problem it solves.

Problem: Passing props to nested components is a pain

Imagine we wanted to pass a theme prop down to a <ThemedButton> component several levels down the component tree. The long-winded way would be to pass the theme prop at every level like so:

// We pass the `theme` prop to <Toolbar>
const App = () => <Toolbar theme="dark" />;

const Toolbar = ({ theme }) => (
  // The Toolbar component must take an extra `theme` prop
  // and pass it to the ThemedButton component. This can become
  // painful if every single button in the app needs to know
  // the theme because it would have to be passed through all
  // components!
  <div>
    <ThemedButton theme={theme} />
  </div>
);

// We finally get to the ThemedButton
const ThemedButton = ({ theme }) => <Button theme={theme} />;

How Context solves the prop-drilling problem

Passing props down several levels like in the example above is known as prop-drilling and whilst the above example is not too many levels deep, things can get out of hand when we have many components that are nested several levels deep.

Context allows us to pass a value deep into the component tree without passing it through every component.

Here’s the same example rewritten to utilize the Context API.

// Create a context for the current theme
// (with "light" as the default).
const ThemeContext = createContext('light');

const App = () => (
  // By wrapping Toolbar with a ThemeContext.Provider,
  // we allow Toolbar and all its children to access
  // ThemeContext.
  <ThemeContext.Provider value="dark">
    <Toolbar />
  </ThemeContext.Provider>
);

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
const Toolbar = () => (
  <div>
    <ThemedButton />
  </div>
);

const ThemedButton = () => {
  // Uses the value of the `value` prop from the
  // nearest <ThemeContext.Provider>
  // (defined in <App /> in this case)
  const theme = useContext(ThemeContext);

  return <Button theme={theme} />;
};

Updating Context from nested components

Just avoiding the prop-drilling problem already makes Context very useful. What makes it even more powerful is the ability for deeply nested components to be able to update any parent Context.

This ability to update a ‘global’ Context from anywhere in the component tree allows us to implement state management patterns like Flux. In fact, react-redux, the React implementation of Redux, uses the Context API under the hood.

Example: Simple global state management with Context

Let’s take our previous example further by exploring how we can update our ThemeContext’s theme value from inside a nested component.

You can view the final code in the Github repo or edit it on CodeSandbox here.

1. Creating the Context

First, we create a ThemeContext to store our theme – ‘light’ or ‘dark’.

/// theme-context.js
import React from 'react';

const ThemeContext = React.createContext({ theme: 'light' });

export default ThemeContext;

2. Sharing our Context with Context.Provider

Now, we wrap our component tree inside a ThemeContext.Provider in order to share the ThemeContext with all nested components.

In React terminology, all these components inside our ThemeContext.Provider are known as Consumers which means that whenever the value prop of our ThemeContext.Provider changes, all these consumers will re-render.

// App.js
const App = () => {
  const [theme, setTheme] = useState('dark');

  const toggleTheme = () => {
    const newTheme = theme === 'dark' ? 'light' : 'dark';
    setTheme(newTheme);
  };

  const themeContext = {
    theme,
    // We pass the `toggleTheme` function through Context to
    // allow nested components to update the `theme` value.
    toggleTheme
  };

  return (
    // By wrapping <Content> inside <ThemeContext.Provider>,
    // we give <Content> and all its descendents access to
    // ThemeContext's value prop.
    <ThemeContext.Provider value={themeContext}>
      <Content />
    </ThemeContext.Provider>
  );
};

export default App;
// Content.js

// Notice how our `Content` component doesn't have to pass
// the theme down to `NestedContent`.
const Content = () => <NestedContent />;

export default Content;

3. Consuming / Updating the Context with useContext

// NestedContent.js
const NestedContent = () => {
  // Allows us to access the `value` prop of the nearest
  // ThemeContext.Provider
  const { theme, toggleTheme } = React.useContext(ThemeContext);

  return (
    <>
      <h2>Current theme: {theme}</h2>
      <button onClick={toggleTheme}>Toggle theme</button>
    </>
  );
};

export default NestedContent;

Conclusion

Whilst the current Context API could probably be simpler to work with, it nevertheless solves a very real and common problem – having a global store that can be used and updated by many components at different nesting levels.

If you find yourself passing down a set of props through many layers, then it’s probably a sign that you should consider using Context!