Ibrahima Ndaw JavaScript enthusiast, full-stack developer, and blogger who also dabbles in UI/UX design.

Pitfalls of overusing React Context

4 min read 1349

Pitfalls of Overusing React Context

For the most part, React and state go hand-in-hand. As your React app grows, it becomes more and more crucial to manage the state.

With React 16.8 and the introduction of hooks, the React Context API has improved markedly. Now we can combine it with hooks to mimic react-redux; some folks even use it to manage their entire application state. However, React Context has some pitfalls and overusing it can lead to performance issues.

In this tutorial, we’ll review the potential consequences of overusing React Context and discuss how to use it effectively in your next React project.

What is React Context?

React Context provides a way to share data (state) in your app without passing down props on every component. It enables you to consume the data held in the context through providers and consumers without prop drilling.

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount(counter => counter + 1);
  const decrement = () => setCount(counter => counter - 1);
  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </CounterContext.Provider>
  );
};

const IncrementCounter = () => {
  const { increment } = React.useContext(CounterContext);
  return <button onClick={increment}> Increment</button>;
};

const DecrementCounter = () => {
  const { decrement } = React.useContext(CounterContext);
  return <button onClick={decrement}> Decrement</button>;
};

const ShowResult = () => {
  const { count } = React.useContext(CounterContext);
  return <h1>{count}</h1>;
};

const App = () => (
  <CounterProvider>
    <ShowResult />
    <IncrementCounter />
    <DecrementCounter />
  </CounterProvider>
);

Note that I intentionally split IncrementCounter and DecrementCounter into two components. This will help me more clearly demonstrate the issues associated with React Context.

As you can see, we have a very simple context. It contains two functions, increment and decrement, which handle the calculation and the result of the counter. Then, we pull data from each component and display it on the App component. Nothing fancy, just your typical React app.

Gordon Ramsay Asking, "What Is the Problem?"

From this perspective, you may be wondering what’s the problem with using React Context? For such a simple app, managing the state is easy. However, as your app grows more complex, React Context can quickly become a developer’s nightmare.

Pros and cons of using React Context

Although React Context is simple to implement and great for certain types of apps, it’s built in such a way that every time the value of the context changes, the component consumer rerenders.

So far, this hasn’t been a problem for our app because if the component doesn’t rerender whenever the value of the context changes, it will never get the updated value. However, the rerendering will not be limited to the component consumer; all components related to the context will rerender.

To see it in action, let’s update our example.

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  const [hello, setHello] = React.useState("Hello world");

  const increment = () => setCount(counter => counter + 1);
  const decrement = () => setCount(counter => counter - 1);

  const value = {
    count,
    increment,
    decrement,
    hello
  };

  return (
    <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
  );
};

const SayHello = () => {
  const { hello } = React.useContext(CounterContext);
  console.log("[SayHello] is running");
  return <h1>{hello}</h1>;
};

const IncrementCounter = () => {
  const { increment } = React.useContext(CounterContext);
  console.log("[IncrementCounter] is running");
  return <button onClick={increment}> Increment</button>;
};

const DecrementCounter = () => {
  console.log("[DecrementCounter] is running");
  const { decrement } = React.useContext(CounterContext);
  return <button onClick={decrement}> Decrement</button>;
};

const ShowResult = () => {
  console.log("[ShowResult] is running");
  const { count } = React.useContext(CounterContext);
  return <h1>{count}</h1>;
};

const App = () => (
  <CounterProvider>
    <SayHello />
    <ShowResult />
    <IncrementCounter />
    <DecrementCounter />
  </CounterProvider>
);

I added a new component, SayHello, which displays a message from the context. We’ll also log a message whenever these components render or rerender. That way, we can see whether the change affects all components.

// Result of the console
 [SayHello] is running
 [ShowResult] is running
 [IncrementCounter] is running
 [DecrementCounter] is running

When the page finishes loading, all messages will appear on the console. Still nothing to worry about so far.

Let’s click on the increment button to see what happens.

// Result of the console
 [SayHello] is running
 [ShowResult] is running
 [IncrementCounter] is running
 [DecrementCounter] is running

As you can see, all the components rerender. Clicking on the decrement button has the same effect. Every time the value of the context changes, all components’ consumers will rerender.

You may still be wondering, who cares? Isn’t that just how React Context works?

Joey From "Friends" Shrugging

For such a tiny app, we don’t have to worry about the negative effects of using React Context. But in a larger project with frequent state changes, the tool creates more problems than it helps solve. A simple change would cause countless rerenders, which would eventually lead to significant performance issues.

So how can we avoid this performance-degrading rerendering?

Prevent rerendering with useMemo()

Maybe memorization is the solution to our problem. Let’s update our code with useMemo to see if memorizing our value can help us avoid rerendering.

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  const [hello, sayHello] = React.useState("Hello world");

  const increment = () => setCount(counter => counter + 1);
  const decrement = () => setCount(counter => counter - 1);

  const value = React.useMemo(
    () => ({
      count,
      increment,
      decrement,
      hello
    }),
    [count, hello]
  );

  return (
    <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
  );
};

const SayHello = () => {
  const { hello } = React.useContext(CounterContext);
  console.log("[SayHello] is running");
  return <h1>{hello}</h1>;
};

const IncrementCounter = () => {
  const { increment } = React.useContext(CounterContext);
  console.log("[IncrementCounter] is running");
  return <button onClick={increment}> Increment</button>;
};

const DecrementCounter = () => {
  console.log("[DecrementCounter] is running");
  const { decrement } = React.useContext(CounterContext);
  return <button onClick={decrement}> Decrement</button>;
};

const ShowResult = () => {
  console.log("[ShowResult] is running");
  const { count } = React.useContext(CounterContext);
  return <h1>{count}</h1>;
};

const App = () => (
  <CounterProvider>
    <SayHello />
    <ShowResult />
    <IncrementCounter />
    <DecrementCounter />
  </CounterProvider>
);

Now let’s click on the increment button again to see if it works.

<// Result of the console
 [SayHello] is running
 [ShowResult] is running
 [IncrementCounter] is running
 [DecrementCounter] is running

Unfortunately, we still encounter the same problem. All components’ consumers are rerendered whenever the value of our context changes.

Michael Scott Making a Sad Face

If memorization doesn’t solve the problem, should we stop managing our state with React Context altogether?

Should you use React Context?

Before you begin your project, you should determine how you want to manage your state. There are myriad solutions available, only one of which is React Context. To determine which tool is best for your app, ask yourself two questions:

  1. When should you use it?
  2. How do you plan to use it?

If your state is frequently updated, React Context may not be as effective or efficient as a tool like React Redux. But if you have static data that undergoes lower-frequency updates such as preferred language, time changes, location changes, and user authentication, passing down props with React Context may be the best option.

If you do choose to use React Context, try to split your large context into multiple contexts as much as possible and keep your state close to its component consumer. This will help you maximize the features and capabilities of React Context, which can be quite powerful in certain scenarios for simple apps.

So, should you use React Context? The answer depends on when and how.

Final thoughts

React Context is an excellent API for simple apps with infrequent state changes, but it can quickly devolve into a developer’s nightmare if you overuse it for more complex projects. Knowing how the tool works when building highly performant apps can help you determine whether it can be useful for managing states in your project. Despite its limitations when dealing with a high frequency of state changes, React Context is a very powerful state management solution when used correctly.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibrahima Ndaw JavaScript enthusiast, full-stack developer, and blogger who also dabbles in UI/UX design.

2 Replies to “Pitfalls of overusing React Context”

  1. How about using three different context(for value, increment and decrement) that value object will not recreated every time count value is changed thus stop unnecessary re-rendering of child nodes

    import React, { useState, useContext, useCallback } from “react”;

    const CounterContext = React.createContext();
    const CounterIncreaseAction = React.createContext();
    const CounterDecreaseAction = React.createContext();

    const CounterProvider = ({ children }) => {
    const [count, setCount] = useState(0);

    const incerement = useCallback(() => setCount(prev => prev + 1), []);
    const decrement = useCallback(() => setCount(prev => prev – 1), []);

    return (

    {children}

    );
    };

    export const useCounter = () => useContext(CounterContext);
    export const useCounterIncrease = () => useContext(CounterIncreaseAction);
    export const useCounterDecrease = () => useContext(CounterDecreaseAction);

    export default CounterProvider;

    and

    import React from “react”;
    import ReactDOM from “react-dom”;
    import CounterProvider, {
    useCounter,
    useCounterIncrease,
    useCounterDecrease
    } from “./counter-provider”;

    const IncrementCounter = () => {
    console.log(“inceremtn rendered”);
    const incerement = useCounterIncrease();
    return Increment;
    };

    const DecrementCounter = () => {
    console.log(“decrement rendered”);
    const decrement = useCounterDecrease();
    return Decrement;
    };

    const ShowResult = () => {
    console.log(“result rendered”);
    const count = useCounter();
    return {count};
    };

    const App = () => (

    );

Leave a Reply