Reputation: 791
This is a very common performance problem while using the Context API. Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown.
If I have a the wrapper as this:
<CounterProvider>
<SayHello />
<ShowResult />
<IncrementCounter />
<DecrementCounter />
</CounterProvider>
And the value props as:
<CounterContext.Provider value={{increment, decrement, counter, hello }} >
{children}
</CounterContext.Provider>
Everytime I increment the count value from the IncrementCounter
component, the entire set of wrapped components re-renders as it is how the Context API is supposed to work.
I did a bit of research and came across these solutions:
React.Memo
: I saw a lot of articles suggesting to the React.Memo API as follows:<CounterContext.Provider
value={useMemo(
() => ({ increment, decrement, counter, hello }),
[increment, decrement, counter, hello]
)}
>
{children}
</CounterContext.Provider>
This however doesn't work as expected. I still can see all the components getting re-rendered. What I'm doing wrong while using the Memo API? Dan Abramov does recommend to go by this approach in an open React issue
If anyone can help me out on this one. Thanks for reading.
Upvotes: 12
Views: 8361
Reputation: 46211
"Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown."
The above statement is true if a context is used like in the below example where components are directly nested in the provider. All of them re-render when count
changes, no matter whether they have called useContext(counterContext)
or not.
const counterContext = React.createContext();
const CounterContextProvider = () => {
const [count, setCount] = React.useState(0);
return (
<counterContext.Provider value={{ count, setCount }}>
<button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
<ComponentOne/>
<ComponentTwo />
</counterContext.Provider>
);
};
const ComponentOne = () => {
console.log("ComponentOne renders");
return <div></div>;
};
const ComponentTwo = () => {
console.log("ComponentTwo renders ");
return <div></div>;
};
function App() {
return (
<CounterContextProvider/>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
"Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown."
The above statement is false if you are consuming nested components with the children
prop, like in the below example.
This time when count
changes, CounterContextProvider
renders, but since it's rendering because its state has changed and not because of its parent rendering, and because a component cannot mutate its props
, React won't render children
.
That's it if it was a normal component. But since there is a context involved here, React will find all components that contain useContext(counterContext)
and render them.
const counterContext = React.createContext();
const CounterContextProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
return (
<counterContext.Provider value={{ count, setCount }}>
<button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
{children}
</counterContext.Provider>
);
};
const ComponentOne = () => {
const { count } = React.useContext(counterContext);
console.log("ComponentOne renders");
return <div></div>;
};
const ComponentTwo = () => {
console.log("ComponentTwo renders ");
return <div></div>;
};
function App() {
return (
<CounterContextProvider>
<ComponentOne />
<ComponentTwo />
</CounterContextProvider>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In the above example only ComponentOne
renders when count
changes, which is normal cause he is consuming it. Every component that calls useContext(counterContext)
renders if one value of the context changes.
Even with useMemo
wrapping the context value
object as you did, that's the behavior you get as soon as one variable in its dependency array changes.
If this is bothering you, it's recommended to have multiple simple contexts, instead of one with a value
object that has multiple key-value pairs.
Upvotes: 16