I have developed a library for managing state through Context with React in an easy and optimal way. Its name is react-context-slices. It behaves great and optimal, as far as I know and I could have tested and checked. But here is my question, if you look at the source code I do:
return (
<StateContext.Provider value={{ [name]: state }}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
and the behaviour or result of this is that the components fetching or consuming this Context (StateContext
) only updates or re-renders when state
changes, which is great, because is what I want and is the optimal behaviour. But I can't figure it out why this happens. In theory, I should have done something like this:
const value = useMemo(()=>({[name]: state}), [name, state]);
return (
<StateContext.Provider value={value}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
But as I say in this case the behaviour, as far as I could have tested, is the same using useMemo
or not using it. And I can not understand why the first case, without useMemo
, behaves as it does, that is, only updating (rendering) components consuming the StateContext
when state
changes.
I would understand this behaviour if the source code were:
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
which is not. As I say the value passed to the provider is {[name]: state}
and not state
, but the behaviour is as if I passed state
and not {[name]: sate}
.
Can anyone explain to me why is this "good/optimal" behaviour happening.
Apparently, this is the correct behavior. It looks like no matter what value you pass to the Provider of the Context, the component consuming it will only re-render if the state defined in the Provider wrapper component changes. For example:
// 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;
// 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;
In the above situation, each time you press the =
button, rendering provider
gets written in the console. But no value
is written in the console, that's it, App
doesn't rerender. It is not until you change the state in the provider by pressing not =
button, that the App
gets rendered again.
Here is the sequence:
sequence in the console result of the interaction with the buttons = and not =
So the conclusion is that when using Context like this, it only updates components consuming it when state
changes, not when value
changes. And the reason for this is what @super pointed me to in his comment to my own question. Is written in the React documentation 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 "value" refers to the state
value. This explains why the rendering provider
appeared in the console, as the sentence "React may still need to call your component before ignoring the result" says.
Thanks @super for giving me the answer to this apparent mystery.
I must add that you have to keep in mind that despite React ignoring the result, the component was called, so if you change the current
value of a ref in its execution (or change a global var as I did), it will stay changed after that. This is something to keep in mind.