You might not need React Context

Categories:

React 16.3 finally made the Context API stable. Since then, many developers started using it to solve the “prop-drilling” problem - the issue where you need to pass a prop down the component tree along components not making use of it in order to access that prop in a component being rendered deep down the tree. The React documentation states the intent of the Context API as follows:

In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like this between components without having to explicitly pass a prop through every level of the tree.

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

Let’s look at the example of tracking the activity of the user. For that, we want to call a certain track function every time the user clicks on a UI element.

Tracker in plain React

const track = event => console.log(`${event} occured`);

class App extends React.Component {
  render() {
    return <Toolbar track={track} />;
  }
}

function Toolbar(props) {
  return (
    <div>
      <TrackedButton track={props.track} />
    </div>
  );
}

function TrackedButton(props) {
  const onClick = () => {
    props.track("button click");
    // do something on click
  };
  return <Button onClick={onClick} />;
}

We need the track function in many different components of the app, and using React without any advanced patterns we need to pass track through the whole component tree, even though App and Toolbar make no use of it.

Tracker with Context

We can use the new React Context API to inject the track function into the needed components:

const track = event => console.log(`${event} occured`);
const TrackerContext = React.createContext(track);

class App extends React.Component {
  render() {
    return (
      <TrackerContext.Provider value={track}>
        <Toolbar />
      </TrackerContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      {/* does not need to forward props anymore */}
      <TrackedButton />
    </div>
  );
}

function TrackedButton(props) {
  return (
    <TrackerContext.Consumer>
      {value => (
        <Button
          onClick={() => {
            // call track
            value("button click");
            // do something on click
          }}
        />
      )}
    </TrackerContext.Consumer>
  );
}

We create a TrackerContext, wrap the root component with Provider which makes the track function consumable as value in the TrackedButton component.

However, this is not something we couldn’t do prior to the Context API, you could always do this with a HOC (Higher Order Component) in React. The code is even more readable using a HOC.

Tracker as a HOC

We can create a HOC, a function taking a component and returning a new component rendering the original component with some enhancements. In our case, we will just inject the track function as a prop into the inner component.

const track = event => console.log(`${event} occured`);
const withTracker = track => Component => (props) => (
  <Component track={track} {...props}/>
);

class App extends React.Component {
  render() {
    return (
      /* no need to wrap root Component */
      <Toolbar />
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      {/* need to use the HOC here */}
      <TrackedButtonWrapped />
    </div>
  );
}

// TrackedButton is the same as in first solution
function TrackedButton(props) {
  const onClick = () => {
    props.track("button click");
    // do something on click
  };
  return <Button onClick={onClick} />;
}
// Need to create a higher-order component out of TrackedButton
const TrackedButtonWrapped = withTracker(track)(TrackedButton)

The nice thing about this is that the code is a lot cleaner than with using Context:

  1. App needs no ContextProvider wrapper anymore.
  2. TrackedButton is exactly the same component as in the plain React solution. It’s not its responsibility anymore to retrieve the track function.

This is because we shifted the prop retrieval into the TrackedButtonWrapped HOC which we render in Toolbar instead.

So is React.createContext useless?

Let’s go back to the example given in the React documentation. There, Context is used to theme an app.

Theming with Context

The code is similar to the Tracking with Context example. ThemeContext.Provider provides the theme and a function to toggle the theme which sets state in the root component. These variables are only consumed where needed - in the ThemedButton to style the button and toggle the theme on button click.

const ThemeContext = React.createContext();

class App extends React.Component {
  state = {
    theme: 'light'
  }

  toggleTheme = () => {
    this.setState(({ theme }) => ({
      theme: theme === 'light' ? 'dark' : 'light',
    }));
  }

  render() {
    const value = {
      theme: this.state.theme,
      toggleTheme: this.toggleTheme,
    }
    return (
      <ThemeContext.Provider value={value}>
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {({ theme, toggleTheme }) => <Button theme={theme} onClick={toggleTheme}/>}
    </ThemeContext.Consumer>
  );
}

Theming as a HOC: First try

Let’s try to apply the same HOC pattern that we did with Tracking to Theming.

Spoiler: It won’t work.

const withTheme = InnerComponent => class extends React.Component {
  state = {
    theme: 'light'
  }

  toggleTheme = () => {
    this.setState(({ theme }) => ({
      theme: theme === 'light' ? 'dark' : 'light',
    }));
  }

  render() {
    return (
      <InnerComponent theme={this.state.theme} toggleTheme={this.toggleTheme} />
    )
  }
}

class App extends React.Component {
  render() {
    return (
      // again no Provider needed
        <Toolbar />
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      {/* use the HOC here */}
      <ThemedButtonWrapped />
      {/* let's add another Button here */}
      <ThemedButtonWrapped />
    </div>
  );
}

function ThemedButton(props) {
  return <Button onClick={props.toggleTheme} theme={props.theme} />;
}
// Need to create a higher-order component out of ThemedButton
const ThemedButtonWrapped = withTheme(ThemedButton)

What’s wrong here? If we add another ThemedButton, you’ll notice that it doesn’t work here. Every time the HOC is created, the component instance starts with a fresh state, and so the buttons’ themes are independent of each other. You can play around with it here.

Why did it work in the tracking example? In the tracking example the data (track function) is static and never changed, so each Button instance using the same function is not a problem here.

This is the big advantage of the Context API over the HOC pattern and why it is so powerful: Using Context the data is shared among all Consumers.

React Context vs HOCs

Summary

Here’s again what the React documentation says about the use-cases of Context:

Context is designed to share data that can be considered “global” for a tree of React components

I would go one step further and say that the Context API is for global dynamic data that is used by multiple component instances. In the case of static data, you might not need Context. It can always be replaced with a simpler to use HOC that injects the props.

Here’s a general guideline how to decide whether to use React’s Context API:

React Context Flowchart

Hi, I'm Christoph Michel 👋

I'm a , , and .

Currently, I mostly work in software security and do on an independent contractor basis.

I strive for efficiency and therefore track many aspects of my life.