...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;
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;
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:
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>
);
};
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:
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)