React's Context API re-renders all components that are wrapped inside the Context

Viewed 178

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:

  1. Split the Context into N number of Context according to the use-case : This solution works as expected.
  2. Wrap the value provider using 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.

1 Answers

"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 wether they 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 statement is false if you are consuming nested components with children. This time when count changes CounterContextProvider renders, but since it's rendering because its state has changed and not because his parent rendering, and because a component cannot mutate its props, React won't render children. That's it if it was a normal parent component.

But since there is a context involved here, React will find all components that contains 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 object as you did that's the behaviour you get as soon as one variable in its dependency array changes. That's why context is designed to hold data that doesn't change so often, like the logged in user or the language preference.

Related