Overview

Typical usage of React Context

React 16 introduced Context API as a means to share data among multiple components, regardless of its depth in the component hierarchy.

With the introduction of Hooks, it can effectively be used as a replacement for Redux, in cases where you don’t want to bring in an external dependency, or when the complexity of Redux makes you dizzy.

In this short article, let’s investigate why React Context did not quite work as we intended, and what we did to fix that.

Defining your Context and Provider

Below I have created a Context Provider for our simple application. This context provides a counter and functions to increment and decrement its value. We also have a counter name and its setter. This code is also available in this Code Sandbox.

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
35
import React, { useState } from "react";

const defaultValue = {
  counter: 0,
  incrementCounter: () => {},
  decrementCounter: () => {},
  name: "",
  setName: () => {}
};

export const AppContext = React.createContext(defaultValue);

const AppContextProvider = (props) => {
  const [counter, setCounter] = useState(0);
  const [name, setName] = useState("");
  return (
    <AppContext.Provider
      value={
        counter,
        incrementCounter: () => {
          setCounter((ctr) => ctr + 1);
        },
        decrementCounter: () => {
          setCounter((ctr) => (ctr > 0 ? ctr - 1 : 0));
        },
        name,
        setName
      }
    >
      {props.children}
    </AppContext.Provider>
  );
};

export default AppContextProvider;

The Context Provider is meant to wrap your consumer components with it so that the context will be made available to all components wrapped by it.

Consuming your Context

First we have a Card component that renders our card component containing our counter and buttons to increment and decrement it. The useContext call brings in AppContext into this component.

You know when a component state and props changes, the component re-renders? This is the same with components that consume a context. When the context changes, the component that consumes that context will get a re-render to give it a chance to update itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useContext, useEffect } from "react";
import { AppContext } from "./AppContext";

export const Card = ({ name }) => {
  const context = useContext(AppContext);
  useEffect(() => {
    context.setName(name);
  });

  return (
    <>
      <div>{`Counter: ${context.name}`}</div>
      <div>{`Counter value: ${context.counter}`}</div>
      <button onClick={context.incrementCounter}>Increment</button>
      <button onClick={context.decrementCounter}>Decrement</button>
    </>
  );
};

In the snippet below, I have wrapped each consumer component Card with our AppContextProvider. I want to create 3 counters that all point to the same global context.

However, to my surprise the code below does not work as intended. Can you guess why?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import AppContextProvider from "./AppContext";
import { Card } from "./Card";
import "./styles.css";

export default function UsingContext() {
  const counters = [1, 2, 3];
  return (
    <div className="App">
      <h1>Using React Context</h1>
      {counters.map((c) => {
        return (
          <div key={c} className="Counter">
            <AppContextProvider>
              <Card name={`Counter ${c}`} />
            </AppContextProvider>
          </div>
        );
      })}
    </div>
  );
}

Clicking any of the Increment or Decrement button is meant to update the counters. Since all the counters are supposed to be pointing to the same context, all the counters should change together. However this isn’t what is happening.

alt text

It turns out that in the above code, we are actually creating 3 separate contexts. And useContext will get the context of the first Context provider up the hierarchy.

To fix this issue, instead of creating multiple contexts, we need to move the AppContextProvider up the tree to create the single context, effectively creating the global context that all the counters share. Like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import AppContextProvider from "./AppContext";
import { Card } from "./Card";
import "./styles.css";

export default function UsingContext() {
  const counters = [1, 2, 3];
  return (
    <div className="App">
      <h1>Using React Context</h1>
      <AppContextProvider>
        {counters.map((c) => {
          return (
            <div key={c} className="Counter">
              <Card name={`Counter ${c}`} />
            </div>
          );
        })}
      </AppContextProvider>
    </div>
  );
}

Conclusion

In this post I have demonstrated how to create a Context, wrap a component hierarchy in it so that it functions as a Context Provider.

We have also seen how easy it is to consume that context from a component under that hierarchy. Once a component has been made a context consumer, that component will then get a re-render when it detects changes to the context, similar to when a state or props changes in a React component.

Care must be taken though when creating that provider. You can create multiple instances of that context, so that each instance will have its own separate state. However, if your intention is to create a Global context, create one copy of the context just like when you create a global Javascript variable.

Resources

2021

Back to top ↑

2020

DynamoDB and Single-Table Design

9 minute read

Follow along as I implement DynamoDB Single-Table Design - find out the tools and methods I use to make the process easier, and finally the light-bulb moment...

Back to top ↑

2019

Website Performance Series - Part 3

5 minute read

Speeding up your site is easy if you know what to focus on. Follow along as I explore the performance optimization maze, and find 3 awesome tips inside (plus...

Back to top ↑