How to use React Context like a pro
Tags: react, javascript
Published: 2021-09-13 19:04:29

Using [React's Context API](https://reactjs.org/docs/context.html) is often very useful. I've found that there are several patterns that you should regularly use in combination with contexts in React, and these patterns aren't that well known.

I'm sharing them in this article so you can start using React Contexts like a pro!

As a quick reminder before we start, here's the vanilla hook based implementation of a context (that we'll use as an example for the different patterns):

```jsx
import React, { useContext, createContext, useState, useEffect } from 'react';

// create context
const UserContext = createContext(null);

const App = () => {
  // the value that will be given to the context
  const [user, setUser] = useState(null);

  // fetch a user from a fake backend API
  useEffect(() => {
    const fetchUser = () => {
      // this would usually be your own backend, or localStorage
      // for example
      fetch('https://randomuser.me/api/')
        .then((response) => response.json())
        .then((result) => setUser(result.results[0]))
        .catch((error) => console.log('An error occurred');
    };

    fetchUser();
  }, []);

  return (
    // the Provider gives access to the context to its children
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
};

const Page = () => {
  // access the context value
  const user = useContext(UserContext);

  if (user?.login?.username) {
    return <p>You are logged in as {user?.login.username}</p>;
  } else {
    return <p>You are not logged in</p>;
  }
};

export default App;
```

In this example, the context is used to provide the logged-in `user` object to the app. This context is then consumed by the `Page` component that conditionally renders based on the `user` value.

This is a very common use case in real-life React applications.

Let's see how we can improve it.

## Extract the React Context logic in another file

One thing I don't like in the code above is that the context logic is mixed in with the `App` code when both have little to do with each other. The `App` only wants to provide the context to its children and doesn't care about how this context is made.

So let's extract all of this logic to an external file.

### Use React Context with a custom Provider

First, we'll create a `UserContextProvider` component inside of a new file called `UserContext.jsx`.

This component is the one that will hold the logic for getting the value of the context (`user`) and giving it to the `UserContext.Provider`:

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

// create context
const UserContext = createContext(null);

const UserContextProvider = ({ children }) => {
  // the value that will be given to the context
  const [user, setUser] = useState(null);

  // fetch a user from a fake backend API
  useEffect(() => {
    const fetchUser = () => {
      // this would usually be your own backend, or localStorage
      // for example
      fetch("https://randomuser.me/api/")
        .then((response) => response.json())
        .then((result) => setUser(result.results[0]))
        .catch((error) => console.log("An error occured"));
    };

    fetchUser();
  }, []);

  return (
    // the Provider gives access to the context to its children
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
};

export { UserContext, UserContextProvider };
```

Now that we're removed the above from our `App` component, it's way cleaner:

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

import { UserContext, UserContextProvider } from "./UserContext";

const App = () => {
  return (
    <UserContextProvider>
      <Page />
    </UserContextProvider>
  );
};

const Page = () => {
  // access the context value
  const user = useContext(UserContext);

  if (user?.login?.username) {
    return <p>You are logged in as {user?.login.username}</p>;
  } else {
    return <p>You are not logged in</p>;
  }
};

export default App;
```

Isn't it much nicer?

### Use React Context with a custom hook

Unfortunately, there's still something bothering me in the code above.

In the `Page` component, we are accessing the context by using the `useContext` hook directly. But what if the component is not actually inside a `UserContextProvider`?

Then the value would default to `undefined` without us knowing. Of course, we could do a check for that in the `Page` component, but that means we would have to do it in every context consumer, which would get annoying.

It's much simpler to extract the `useContext` line to a custom hook, and we will do the check there.

Of course, you could argue that as our `UserContextProvider` is at the top-level of our app it's unlikely that a component would live outside of it.

Fair, but keep in mind that contexts aren't always at the top level. It's quite common for contexts to only be available in a section of the app, and in those cases it's quite easy to use a context where it's not available.

Another benefit to doing that is that it saves us an import. Instead of having to import both the `useContext` hook and the actual context itself (`UserContext`), we now only have to import the custom consumer hook. Fewer lines to write! 😄

Here's the resulting custom consumer hook:

```jsx
// context consumer hook
const useUserContext = () => {
  // get the context
  const context = useContext(UserContext);

  // if `undefined`, throw an error
  if (context === undefined) {
    throw new Error("useUserContext was used outside of its Provider");
  }

  return context;
};
```

And to use it, simply import the hook and use it in the `Page` component:

```jsx
const Page = () => {
  // access the context value
  const user = useUserContext();

  if (user?.login?.username) {
    return <p>You are logged in as {user?.login.username}</p>;
  } else {
    return <p>You are not logged in</p>;
  }
};
```

If you ask me, our context usage now seems very nice! All of the logic related to the `UserContext` sits in one file, the context is very simple to access using the `useUserContext` hook and we will be warned whenever we try to access the context outside of the right provider.

The code above is usually enough for most purposes, but sometimes you need to go further, usually for performance and optimization reasons.

The next two sections explore ways to optimize your context. Bear in mind that it should only be used if you are indeed having performance and optimization issues. Otherwise it's safe to go with the simpler option from above.

## Be careful about updating context values, and memoize them

Imagine our `UserContext` in a big app. Presumably, a lot of components are using the context.

Now imagine that we are polling our backend every 15 seconds to see if the `user` value changed somehow. For example we could be storing the number of credits a user has left in his account directly in the `user` object.

Want to learn how to do something every 15 seconds in a React app? You do it [using intervals](https://devtrium.com/posts/set-interval-react)!


If we do this naively, it means that every single component which uses that context will re-render every 15 seconds. Not great.


You might say that polling your backend every 15 seconds for this info and storing it in the `user` object isn't the best way to go about doing this.

And you would be right. However, this situation and others like it frequently happen in production, where things aren't always ideal.

This exact issue happened in my team a few months ago. Knowing how to avoid that problem is an important part of knowing how to effectively use context API in React.

Let's see how to avoid that.

### Memoize values in your context with `useMemo` and `useCallback`

It's usually a good idea to wrap context values with memoizing functions like `useMemo` and `useCallback`.

Context values are often used in dependency arrays in context consumers. If you don't memoize context values, you can end up with unwanted behaviors like `useEffect` triggering unnecessarily.

A change in those values could trigger dependency arrays in every context consumer, so it can have a sizeable impact on the affected components. And memoizing the value in the context is very effective since you only have to memoize the value once and it will work for all the components consuming the context.

Here is [more information on dependency arrays](https://devtrium.com/posts/dependency-arrays) if you are not sure about how they work.

For example, let's say you have a `signout` function inside the context. It's best to wrap it in a `useCallback` hook:

```jsx
const UserContextProvider = ({ children }) => {
  // the value that will be given to the context
  const [user, setUser] = useState(null);

  // sign out the user, memoized
  const signout = useCallback(() => {
    setUser(null);
  }, []);

  // fetch a user from a fake backend API
  useEffect(() => {
    const fetchUser = () => {
      // this would usually be your own backend, or localStorage
      // for example
      fetch("https://randomuser.me/api/")
        .then((response) => response.json())
        .then((result) => setUser(result.results[0]))
        .catch((error) => console.log("An error occured"));
    };

    fetchUser();
  }, []);

  return (
    // the Provider gives access to the context to its children
    <UserContext.Provider value={{ user, signout }}>
      {children}
    </UserContext.Provider>
  );
};
```

Keep in mind that memoizing won't always prevent unnecessary triggers. For example, the `user` variable is an object. If you change that object through a `setState`, as far as `useMemo` is concerned the object is a new one (even if all the keys and the values are the same). This is because React is only doing a shallow equality test in dependency arrays.

In that case, you should do the check yourself and only update the context value if necessary. To do that you could for example use [Lodash's `isEqual`](https://lodash.com/docs/4.17.15#isEqual) function that deeply compares two javascript objects.

## Separate state and state setters (if necessary)

To be clear, you usually don't need to do this. If you're careful about updating context values and they are memoized, you're very probably fine.

But sometimes you might run into issues that will be solved by separating context state and context state setters.

Here's what I mean by "context state" and "context state setter".

In our last example you have the `user` object, which is the "context state", and the `signout` function, which is a "context state setter": it's used to change the "context state".

Both don't need to be in the same provider. For example, a log-out button might only need the `signout` function without caring about the current state of authentication.

In the default case, that button would update every time the `user` object changes, because a change in the `user` object means a change in the context value which means an update to every consumer of the context.

In situations where you care about this (and only in those), you can separate your state and your state setters in two different contexts.

I believe this idea was first introduced by [Kent C. Dodds in this blog post](https://kentcdodds.com/blog/how-to-optimize-your-context-value).

The implementation of that pattern is the following:

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

// create contexts
const UserContextState = createContext(null);
const UserContextUpdater = createContext(null);

// context consumer hook
const useUserContextState = () => {
  // get the context
  const context = useContext(UserContextState);

  // if `undefined`, throw an error
  if (context === undefined) {
    throw new Error("useUserContextState was used outside of its Provider");
  }

  return context;
};

// context consumer hook
const useUserContextUpdater = () => {
  // get the context
  const context = useContext(UserContextUpdater);

  // if `undefined`, throw an error
  if (context === undefined) {
    throw new Error("useUserContextUpdater was used outside of its Provider");
  }

  return context;
};

const UserContextProvider = ({ children }) => {
  // the value that will be given to the context
  const [user, setUser] = useState(null);

  const signout = useCallback(() => {
    setUser(null);
  }, []);

  // fetch a user from a fake backend API
  useEffect(() => {
    const fetchUser = () => {
      // this would usually be your own backend, or localStorage
      // for example
      fetch("https://randomuser.me/api/")
        .then((response) => response.json())
        .then((result) => setUser(result.results[0]))
        .catch((error) => console.log("An error occured"));
    };

    fetchUser();
  }, []);

  return (
    // the Providers gives access to the context to its children
    <UserContextState.Provider value={user}>
      <UserContextUpdater.Provider value={signout}>
        {children}
      </UserContextUpdater.Provider>
    </UserContextState.Provider>
  );
};

export { UserContextProvider, useUserContextState, useUserContextUpdater };
```

The usage is very similar to before, as you can guess. You just have to choose to access the state or the state setters (or both). Of course if you often need both you can also create a hook that provides both out of the box, thus reproducing the previous behavior.

```jsx
···
const Page = () => {
  // access the context value
  const user = useUserContextState();

  if (user?.login?.username) {
    return <p>You are logged in as {user?.login.username}</p>;
  } else {
    return <p>You are not logged in</p>;
  }
};
···
```

## Only use React Context if you really need it

React Context is a great tool, but it can also be dangerous. As it's usually shared between a bunch of components, it can cause performance issues when abused and used for the wrong kind of state.

Most of the time, `useState` is enough for your needs. It's important to know when to use `useState` and when to use `useContext`. And it's not a clear division either; sometimes both work well.

You want `useState` to be your default option for state and only switch to `useContext` if it's necessary.

A good reason to switch to contexts is if the state is accessed by a lot of components.

Bear in mind that to solve the "prop drilling" issue where you are passing props through layers of components, [there are other strategies you can also use](https://reactjs.org/docs/context.html#before-you-use-context).

Examples of good purposes of context:

- Share the authentication state across your app
- Share a theme across your app
- Share a value that is used by a lot of components in a part of your app (for example the current balance of a user in a dashboard where a lot of components are using that balance)

## Wrap up

Voilà! You're now equipped to improve your usage of React Contexts. As you saw, there are a lot of different ways to go about it, and the one you choose really depends on your exact circumstances.

So part of getting good at using React Contexts is also just building experience and reflecting on your code once in a while, to see if you should have done things differently.

Good luck!