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.
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.
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.
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?
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?
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.
If memorization doesn’t solve the problem, should we stop managing our state with React Context altogether?
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:
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowUnderstanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
Matcha, a famous green tea, is known for its stress-reducing benefits. I wouldn’t claim that this tea necessarily inspired the […]
Backdrop and background have similar meanings, as they both refer to the area behind something. The main difference is that […]
14 Replies to "Pitfalls of overusing React Context"
Yes, context is not practically suitable for frequent state updates. The problem you described is nicely solved by Hookstate (https://hookstate.js.org/). Have a look this demo of 10000 cells table updating one cell per every single millisecond: https://hookstate.js.org/performance-demo-large-table
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 = () => (
);
Shouldn’t setCount also be part of the memoization (note the intentional spelling here) since it’s created on a component render/re-render?
This would be a problem if `setCount` refered to the value itself, `count` which would refer to the value inside the original function definition closure. Instead, they’ve used a callback setter which is going to get the fresh value as it’s argument every time.
`setCount(count + 1)` counter + 1)` <– guaranteed fresh.
The best way to keep track of these type of pitfalls? Use the react hooks eslint plugin!
Hello,
Our project has a “multi-step” form, each step may be saved independently with PATCH requests.
At the end of each step, two buttons: prev / next(and save)
On the left side there’s a sidebar where each step shows if it was saved or is yet to complete. When a step is completed you can click it and navigate to it.
But if your current form is dirty you’ll be asked to save or discard.
The issue is, the component that “knows you are leaving”, is independent from the multiple forms being replaced one after the other in the “form area”.
So we have a solution to, upon each Formik render, “observe” its state (i.e. values, dirty, isValid) and lift it up to the store. So the sidebar shows if the step you are is dirty, and also upon clicking on the other steps, if dirty, you get prompted.
We try to leave Redux behind and when trying to migrate this to another solution it gets very complicated. Redux seems smart enough in its memoized selectors and batched dispatches to prevent extra renders. And since we are dispatching on every Formik render (since Formik itself has no other way to be globally “observed”), a solution based on Context API quickly tears to pieces.
What’s your opinion given this use case. Should we stick to Redux?
Thanks!
I think you could have a global state (in the Context) only with the general status of each step (completed, unsaved, etc).
Then, you could use one “Formik” form for each step (with its own state, isDirty, isValidated, submit button, etc), handled inside each step component, that will also make the PATCH calls.
After each step submission or “leaved without saving”, this global status data could be updated in the Context by calling an update function (you can pass callback functions in the Context value, so they can be accessible everywhere is needed).
@Martin it sounds like it’s a little agnostic to your exact state management solution, but I have a bias. Been working on a wrapper around react-hook-form which basically makes it easy to use RHF and follow the company’s design conventions. The general idea here is to put a cap on the firehose. The firehose being the most noisy event wise. Of course, RHF does this itself quite well, but in our company’s forms, we need to enable the submit button ONLY if EVERY input value in the form is valid. What this looks like for me, is memoizing based on the final determined form validity/submitBtn enabled state.
So, identify the most noisy part of the code, that’s firing the most events, and extract out exactly the data you need for the sidebar, and memoize based on that extracted data.
So, you have values, dirty, isValid values you are receiving from formik (your firehose, if I’m thinking about your issue correctly)
Then what you need to know in the sidebar: Some sort of list of the Form Steps saved/incomplete status.
In abstract terms, you need to put a cap on the firehose.
`values` changes on every keystroke.
isFormCompleted changes quite infrequently.
It’s hard to get much more specific, but you would then `useMemo` to avoid any un-necessary cascading due to `values` constantly changing (cascade of change would only happen when isFormCompleted/isFormDirty changes)
Hey Devin, thanks for the comment. It sounds like your solution with the RHF wrapper could be really helpful for other devs as well. Would you be interested in writing a post on it for the LogRocket blog? Let me know — mangelosanto[at]logrocket[dot]com
Can’t see how the same problems that existed with Redux are solved using reselect, for example. Seems like you are bundling multiple states that don’t share the same context into one provider.
Thanks for your precious works.
I made a package here if anyone is interested.
https://www.npmjs.com/package/@fishbot/context-selector
It’s the optimal version of the React Context with Selector, which only re-renders the components that observe the changed value.
This works on both Web and Mobile.
Try @webkrafters/react-observable-context. It handles this particular context re-rendering issues.
I think the issue would be if you use only 1 Provider for all your states. You are supposed to use 1 Provider for each resource/entity of your state, as you would do with reducers in the Redux.
But, for large applications, it would be too many Providers, then Redux is the best option. The new Redux Toolkit made it even easier to setup the store and have “slices” of your data (also, it has RTK Query to manage API calls).
why do the React developers create unstable, unusable stuff. I am frankly pissed out at so many calls. Even though react is beautiful it sucks when there are performance issues becuase developers did not think it through.
https://github.com/lovetingyuan/react-atomic-context
This library can help solve unnecessary re-renders caused by React Context and allows fine-grained control over the reading and writing of each property.