React Context consumer only updates when state defined in Provider wrapper component changes
Published: 2023-06-04 16:54:37

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

## Introduction

After having developed [react-context-slices](https://www.npmjs.com/package/react-context-slices), a library for state management with React (post [here](https://dev.to/roggc/state-management-with-react-hooks-3608)), 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](https://stackoverflow.com/a/76400917/12441000) 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:

```javascript
// 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;
```

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:

```javascript
// 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;
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kydfigtrei0sg1vzaxyr.png)

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](https://react.dev/reference/react/useReducer#setstate-caveats) 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](https://stackoverflow.com/users/7703024/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: 

```javascript
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>
  );
};
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3kkq4qcxepl4r9l35sn.png)
 
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.