DEV Community

roggc
roggc

Posted on

React Context consumer only updates when state defined in Provider wrapper component changes

...and not when value supplied to Context.Provider changes.

Introduction

After having developed react-context-slices, a library for state management with React (post here), I have realised and discovered that fact, that React Context consumers only updates or re-render when state defined in Provider wrapper component changes, not when value supplied to Context.Provider changes (this answer of me in stackoverflow shows it).

Demonstration

In case you don't click the previous link, I will explain it here.

Suppose we have the following code:

// provider.jsx
import { createContext, useReducer, useContext } from "react";

const StateContext = createContext({});
const DispatchContext = createContext(() => {});

const reducer = (state, { type, payload }) => {
  switch (type) {
    case "set":
      return typeof payload === "function" ? payload(state) : payload;
    default:
      return state;
  }
};

export const useMyContext = () => useContext(StateContext);
export const useMyDispatchContext = () => useContext(DispatchContext);

let id = 0;

const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, { foo: "bar" });
  console.log("rendering provider");
  return (
    <StateContext.Provider value={{ ["value" + id++]: state.foo }}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};

export default Provider;
Enter fullscreen mode Exit fullscreen mode

As you can see we define a Provider wrapping component, where the state is created, and we pass a value to the StateContext.Provider that changes with every render of the Provider wrapping component.

Now let's define our consumer, App component:

// app.jsx
import { useMyContext, useMyDispatchContext } from "./provider";

const App = () => {
  const value = useMyContext();
  const dispatch = useMyDispatchContext();
  console.log("value", value);
  return (
    <>
      <button onClick={() => dispatch({ type: "set", payload: (s) => s })}>
        =
      </button>
      <button
        onClick={() => dispatch({ type: "set", payload: (s) => ({ ...s }) })}
      >
        not =
      </button>
      {JSON.stringify(value)}
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

We have two buttons, = and not =. The first doesn't change the state defined in the Provider wrapper component, only not = does. Let's see what happens when interacting with this two buttons. First I will click several times = button, and then, after that, I will click once not = button. This is the result:

console output when interacting with buttons

As you can see the result shows that when pressing = button rendering provider is written to the console, but no value is written to the console, so App doesn't re-render. It's only when pressing not = button that value gets printed in the console. But the fact to notice is that the value of value is {value9: 'bar'} while the previous value printed was {value1: 'bar'}. This shows and demonstrate that despite value in StateContext.Provider changing, the App component (consumer) didn't updated, it only did when state changed.

Explanation (why this happens)

This is some how a surprising behaviour and something a lot of people may not know. The reason for this behaviour is written in the documentation of React regarding the useReducer hook:

If the new value you provide is identical to the current state, as determined by an Object.is comparison, React will skip re-rendering the component and its children. This is an optimization. React may still need to call your component before ignoring the result, but it shouldn’t affect your code.

Here the important part is everything. It says that "React will skip re-rendering the component and its children" based on an "Object.is comparison" of the state value. And also it says that "React may still need to call your component before ignoring the result". This explains why we see the output in the console rendering provider.

Credits to super on stackoverflow for pointing me to it.

A note about the React documentation

The sentence "but it shouldn’t affect your code" isn't entirely true. Suppose I do this:

const Provider = ({ children }) => {
  const idRef = useRef(0);
  const [state, dispatch] = useReducer(reducer, { foo: "bar" });
  console.log("rendering provider");
  return (
    <StateContext.Provider value={{ ["value" + idRef.current++]: state.foo }}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you see I use the useRef hook and change its current value in each execution of the Provider wrapper component.

This is the result in the console:

output in console after interacting with buttons of the app

As you can see it can have effects in our code. The thing is the Provider wrapper component gets executed each time, despite state doesn't change, and the sentence "before ignoring the result" doesn't imply that if you change the current value of a ref or a global value, this will be ignored. No, it will stay changed. Just to know that.

Conclusion

So when using Context.Provider in a wrapping component that creates state with useReducer (or useState also applies) as we have done here (and that is a very usual pattern), there is no need for optimisation with useMemo in the value we pass to the Context.Provider, because if state defined by the useReducer hook doesn't change in an Object.is comparison, then React, despite calling the Provider wrapper component, will ignore the result and will skip re-rendering the wrapping component and its children.

But the sentence in the React documentation "but it shouldn’t affect your code" isn't entirely true. Keep in mind that if you use useRef in the Provider wrapper component, and change its current value in each execution of the Provider component, this will stay changed, so the sentence "before ignoring the result" isn't entirely true or at least we have to keep this side effects in mind (like changing the value of a global variable too).

Thanks for reading and happy coding.

Top comments (0)