React context, performance?
Tags: react, tutorial, javascript, beginners
Published: 2021-11-10 14:04:15

Today we are going to talk about **React context**. Its role is sometimes mistaken, badly said as a mini-redux. Firstly we are going to see what it is, then talk about performance and workarounds we have.

## What is it?

I can't have a better definition than in the documentation:

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

Conceptually, you will put data in a **React context** and provides it to a React sub-tree component thanks to a **Provider**. Then in all components in this sub-tree, you are able to get the data thanks to a **Consumer**. At every changes of the data in the context, each consumers will be notified.

So there is no notion of state management here, so don't be confused, React context is not a mini-redux. But you can simulate it, if you combine it with `state` or `reducer`. However, you have to be aware that **redux** supplies some features like:
- time travelling
- middlewares
- performance optimizations

----

## How to use React context

### Creation of a context

The creation is made thanks to the `createContext` method pulls from `React`. This method takes as only parameter the default value that is optional:

```javascript
const MyContext = React.createContext();
```

----

### Provider

The `Provider` is accessible through the created **context**:

```javascript
const MyProvider = MyContext.Provider;
```

The `Provider` obtained is a **Component** has the following prop:
- a **value**: the value you want to provide to children components
- children: the children to which you want to provide the value

```jsx
<MyProvider value={valueToProvide}>
  {children}
</MyProvider>
```

> **Note:** Make a `Provider` component that takes `children` instead of putting directly components in the Provider like this:

```jsx
function App() {
  const [data, setData] = useState(null);

  return (
    <MyContext.Provider value={{ data, setData }}>
      <Panel>
        <Title />
        <Content />
      </Panel>
    </MyContext.Provider>
  );
}
```

Doing like this each time the `setData` is called, it will render all components `Title`, `Content` and `Panel` even if they do not use the `data`.

So instead do:

```jsx
function MyProvider({ children }) {
  const [data, setData] = useState(null);

  return (
    <MyContext.Provider value={{ data, setData }}>
      {children}
    </MyContext.Provider>
  );
}

function App() {
  return (
    <MyProvider>
      <Panel>
        <Title />
        <Content />
      </Panel>
    </MyProvider>
  );
}
```

----

### Consumer

Once we provide some data, we probably want to get it somewhere in a child. There is 2 ways to get it:
- with `useContext` hook
- with the `Consumer` component provided by the context we created

#### `useContext`

It's the hook to consume value from the context. You just have to pass the context to the hook:

```javascript
const myValue = useContext(MyContext);
```

> **Note:** You may want to create a custom hook `useMyContext` not to export the context directly. It will allow you to make some check to ensure that the `Provider` has well been added:

```javascript
const useMyContext = () => {
  const value = useContext(MyContext);

  if (!value) {
    throw new Error(
      "You have to add the Provider to make it work"
    );
  }

  return value;
};
```

> **Warning:** The check will not work if you put a default value in the context.

#### `Consumer` component

As said previously, the created context exports a `Consumer` component too (like `Provider`), you can then get the value by passing a function as children:

```jsx
<MyContext.Consumer>
  {(value) => {
    // Render stuff
  }
</MyContext.Consumer>
```

----

## Recommendation and property

### Put context the closest to where it's used

An advice is to put `Provider`s the closest to where it's being used. I mean do not put all your `Provider`s at the top of your app. It will help you to dive in the codebase, with separation of concern and should help React to be slightly faster because would not have to cross all the tree components.

> **Note:** You will need to put some `Provider` at the top of your application. For example: `I18nProvider`, `SettingsProvider`, `UserProvider`, ...

Doing this, you will may encounter some performance issues when parent re-render if you pass an object as value (most of the time it will be the case).

For example if you have:

```jsx
const MyContext = React.createContext();

function MyProvider({ children }) {
  const [data, setData] = useState(null);

  const onClick = (e) => {
    // Whatever process
  };

  return (
    <MyContext.Provider value={{ data, onClick }}>
      {children}
    </MyContext.Provider>
  );
}

function ComponentUsingContext() {
  const { onClick } = useContext(MyContext);

  return <button onClick={onClick}>Click me</button>;
}

const MemoizedComponent = React.memo(ComponentUsingContext);

function App() {
  const [counter, setCount] = useState(0);

  return (
    <div>
      <button
        onClick={() => setCounter((prev) => prev + 1)}
      >
        Increment counter: counter
      </button>
      <MyProvider>
        <MemoizedComponent />
      </MyProvider>
    </div>
  );
}
```

In this case, when we increment the counter, `MemoizedComponent` will re-render even it's memoized because the value in the context changes.

In this case the solution is to memoized the value:

```js
const value = useMemo(() => {
  const onClick = (e) => {
    // Whatever process
  };

  return {
    data,
    onClick,
  };
}, [data]);
```

And tada, `MemoizedComponent` do not render anymore when incrementing the counter.

> **Warning:** Memoizing the value is only useful if the components using the context are memoized. Otherwise it will just do nothing and continue re-rendering because parent re-renders (in the previous case the parent is App).


### Nested providers

It's possible to do nested Provider for same context. It's for example used in the `react-router` implementation, [see my article](https://dev.to/romaintrotard/react-router-v6-demystified-part-3-2m1c).

In this case, Consumers will get the value of the closest Provider to them.

```jsx
const MyContext = React.createContext();

export default function App() {
  return (
    <MyContext.Provider value="parent">
      <ParentSubscriber />
      <MyContext.Provider value="nested">
        <NestedSubscriber />
      </MyContext.Provider>
    </MyContext.Provider>
  );
}

function ParentSubscriber() {
  const value = useContext(MyContext);

  return <p>The value in ParentSubscriber is: {value}</p>;
}

function NestedSubscriber() {
  const value = useContext(MyContext);

  return <p>The value in NestedSubscriber is: {value}</p>;
}
```

In the previous example, `ParentSubscriber` will get the value `parent` and in the other side `NestedSubscriber` will get `nested`.

----

## Performance

To talk about performance we are going to do a little music app with few features:
- be able to see what's our friends are listening
- show musics
- show the current music

> **Important:** Do not judge the use of the React context features. I know that for such features we would probably not use React context in real world.

### Friends and musics features

Specifications:
- friends feature consists to fetch every 2sec a fake API that will return an array of object of this type:

```typescript
type Friend = {
  username: string;
  currentMusic: string;
}
```

- musics feature will fetch only once the available music and will return:

```typescript
type Music = {
  uuid: string; // A unique id
  artist: string;
  songName: string;
  year: number;
}
```

Okay. Let's implement this.
Innocently, I want to put all this data in a same **context** and provide it to my application.

Let's implement the Context and Provider:

```javascript
import React, {
  useContext,
  useEffect,
  useState,
} from "react";

const AppContext = React.createContext();

// Simulate a call to a musics API with 300ms "lag"
function fetchMusics() {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve([
          {
            uuid: "13dbdc18-1599-4a4d-b802-5128460a4aab",
            artist: "Justin Timberlake",
            songName: "Cry me a river",
            year: 2002,
          },
        ]),
      300
    )
  );
}

// Simulate a call to a friends API with 300ms "lag"
function fetchFriends() {
  return new Promise((resolve) =>
    setTimeout(() => {
      resolve([
        {
          username: "Rainbow",
          currentMusic:
            "Justin Timberlake - Cry me a river",
        },
      ]);
    }, 300)
  );
}

export const useAppContext = () => useContext(AppContext);

export default function AppProvider({ children }) {
  const [friends, setFriends] = useState([]);
  const [musics, setMusics] = useState([]);

  useEffect(() => {
    fetchMusics().then(setMusics);
  }, []);

  useEffect(() => {
    // Let's poll friends every 2sec
    const intervalId = setInterval(
      () => fetchFriends().then(setFriends),
      2000
    );

    return () => clearInterval(intervalId);
  }, []);

  return (
    <AppContext.Provider value={{ friends, musics }}>
      {children}
    </AppContext.Provider>
  );
}
```

Now let's see the implementations of the `Friends` and `Musics` component. Nothing complicated:

```jsx
function Friends() {
  const { friends } = useAppContext();

  console.log("Render Friends");

  return (
    <div>
      <h1>Friends</h1>
      <ul>
        {friends.map(({ username, currentMusic }) => (
          <li key={username}>
            {username} listening {currentMusic}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

And:

```jsx
function Musics() {
  const { musics } = useAppContext();

  console.log("Render Musics");

  return (
    <div>
      <h1>My musics</h1>
      <ul>
        {musics.map(({ uuid, artist, songName, year }) => (
          <li key={uuid}>
            {artist} - {songName} ({year})
          </li>
        ))}
      </ul>
    </div>
  );
}
```

Now, I will ask you a question. Do you know what will be rendered / print in the console?

Yep, both `Friends` and `Musics` will rendered every around 2sec. Why?
Do you remember that I told you each consumers will be trigger if the value provided changes, even if they use a part of this value that does not change.
It's the case of `Musics` that only pulls `musics`, that does not change, from the context.

You can see it in the following codesandbox:

{% codesandbox pb-same-context-for-multiple-realm-8hppy %}


It's why I advise to **separate** data by **business domain** in **different contexts**.

In our example I will make two separate contexts `FriendsContext` and `MusicContext`.

You can see the implementation here:

{% codesandbox solution-of-multiple-realm-multiple-context-rvymt %}

----


### Current listening music

Now we would like to be able to select a music from the list, and listen it.

I'm going to do a new context to store the `currentMusic`:

```jsx
import React, { useContext, useState } from "react";

const CurrentMusicContext = React.createContext();

export const useCurrentMusicContext = () =>
  useContext(CurrentMusicContext);

export default function CurrentMusicProvider({ children }) {
  const [currentMusic, setCurrentMusic] =
    useState(undefined);

  return (
    <CurrentMusicContext.Provider
      value={{ currentMusic, setCurrentMusic }}
    >
      {children}
    </CurrentMusicContext.Provider>
  );
}
```

I add a button in the `Musics` component to listen the associated music:

```jsx
function MyMusics() {
  const musics = useMusicContext();
  const { setCurrentMusic } = useCurrentMusicContext();

  console.log("Render MyMusics");

  return (
    <div>
      <h1>My musics</h1>
      <ul>
        {musics.map((music) => (
          <li key={music.uuid}>
            {getFormattedSong(music)}{" "}
            <button onClick={() => setCurrentMusic(music)}>
              Listen
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

And the `CurrentMusic` component is simply:

```jsx
function CurrentMusic() {
  const { currentMusic } = useMusicContext();

  console.log("Render CurrentMusic");

  return (
    <div>
      <h1>Currently listening</h1>
      {currentMusic ? (
        <strong>{getFormattedSong(currentMusic)}</strong>
      ) : (
        "You're not listening a music"
      )}
    </div>
  );
}
```

Ok, now what is happening when you chose to listen a new music?

Currently, both `MyMusics` and `CurrentMusic` will render. Because when the `currentMusic` changes a new object is passed to the provider.

{% codesandbox pb-of-static-and-dynamic-data-in-same-context-rrl6e %}


#### Separate `dynamic` and `static` data

One strategy is to separate **dynamic** and **static** data in two different contexts `CurrentMusicDynamicContext` and `CurrentMusicStaticContext`:


```jsx
import React, { useContext, useState } from "react";

const CurrentMusicStaticContext = React.createContext();
const CurrentMusicDynamicContext = React.createContext();

export const useCurrentMusicStaticContext = () =>
  useContext(CurrentMusicStaticContext);
export const useCurrentMusicDynamicContext = () =>
  useContext(CurrentMusicDynamicContext);

export default function CurrentMusicProvider({ children }) {
  const [currentMusic, setCurrentMusic] =
    useState(undefined);

  return (
    <CurrentMusicDynamicContext.Provider
      value={currentMusic}
    >
      <CurrentMusicStaticContext.Provider
        value={setCurrentMusic}
      >
        {children}
      </CurrentMusicStaticContext.Provider>
    </CurrentMusicDynamicContext.Provider>
  );
}
```

And here we go. Just to use the right hook to get value from the context.

{% codesandbox dynamic-context-multiple-contexts-zdfhy %}

> **Note:** As you can see it really complicates the code, that's why do not try to fix performance issue that does not exist. 

----

#### `use-context-selector`

The second solution is to use the library made by **dai-shi** named `use-context-selector`. I made an article about [its implementation](https://dev.to/romaintrotard/use-context-selector-demystified-4f8e).
It will wrap the native context API of React, to give you access to multiple hooks that will re-render your component only if the selected value from the store changed.

The principle is simple, you create your context thanks to the `createContext` function given by the lib. 
Then you select data from it with the `useContextSelector`. The API is:

```js
useContextSelector(CreatedContext, valueSelectorFunction)
```

For example if I want to get the `currentMusic`:

```js
const currentMusic = useContextSelector(
  CurrentMusicContext,
  (v) => v.currentMusic
);
```

Not to expose the context I made a hook:

```js
export const useCurrentMusicContext = (selector) =>
  useContextSelector(CurrentMusicContext, selector);
```

And that's all. You can find the code bellow:


{% codesandbox dynamic-context-use-context-selector-nmfxl %}

----

## Conclusion

We have seen how to use React context, and performance issues that you can encounter. 
But like always, do not do premature optimization. Just try to worry about it when there are real problems.
As you have seen, optimization can make your code less readable and more verbose.
Just try to separate different business logics in different context and to put your provider as close as possible to where it's needed, to make things clearer. Do not put everything at the top of your app.
If you have real performance problems because of contexts, you can:
- separate dynamic and static data in different contexts
- `useMemo` the value if it's changing because of parent re-rendering. But you will have to put some `memo` on components that uses the context (or parent) otherwise it will do nothing.
- use the `use-context-selector` lib to solve context's shortcomings. Maybe one day natively in **`react`** as you can see in [this opened PR](https://github.com/facebook/react/pull/20646).
- one other strategy that we don't talk about it in this article, is not to use React context but **atom state management** library like: `jotai`, `recoil`, ...

---- 


Do not hesitate to comment and if you want to see more, you can follow me on [Twitter](https://twitter.com/romain_trotard ) or go to my [Website](https://www.romaintrotard.com).