react-hooksreact-contextreact-state-managementrerender

Why re-render only happens when state defined in the Provider wrapper component changes, not when value supplied to Context.Provider changes?


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.


Solution

  • 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.