DEV Community

Cover image for Pitfalls of overusing React Context
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Pitfalls of overusing React Context

Written by Ibrahima Ndaw✏️

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>
);
Enter fullscreen mode Exit fullscreen mode

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.

LogRocket Free Trial Banner

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>
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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 — start monitoring for free.


The post Pitfalls of overusing React Context appeared first on LogRocket Blog.

Top comments (5)

Collapse
 
markerikson profile image
Mark Erikson • Edited

We briefly tried using Context to pass down the Redux store state in React-Redux v6, but we ran into these perf problems. That's why we had to go back to direct store subscriptions in components as part of the React-Redux v7 rewrite.

I covered all the history of how and why we use context in React-Redux in my post The History and Implementation of React-Redux.

There's an interesting looking React RFC that was filed to add "context selector" functionality, but I will be surprised if the React team actually decides to do anything about it. Dan Abramov commented on Twitter that he doesn't think a selector-based API is a good approach.

Folks might also want to read through these other articles discussing context and Redux:

Collapse
 
karataev profile image
Eugene Karataev

Having one global context for an app is like providing a full state for every connected component in redux version. In both versions every context consumer/connected component will be re-rendered on any context/state update.
With react-redux it's important to connect a component to minimum slice of a global state to avoid unnecessary re-renders. The similar rule is true for contexts - keep separate contexts for every logical part of an app and avoid using unnecessary contexts in the components.

Collapse
 
sharlos profile image
Chris

Doesn't react-redux use context under the hood? How does it avoid the re-rendering issue?

Collapse
 
markerikson profile image
Mark Erikson

Because we only use context to pass the store instance, not the store state.

I just put up a blog post recently explaining the difference:

React, Redux, and Context Behavior

And for complete details on how React-Redux actually works internally, see my post The History and Implementation of React-Redux.

Collapse
 
joserfelix profile image
Jose Felix

Hi, great post!

Dan Abramov gives three solutions to this problem, check this issue.