Search code examples
reactjsreact-context

Why is the state within a react context always the initial state


Original Question

I am trying to learn more about the inner workings of React, and here specifically Context Providers so I don't have a use-case in mind, just trying to understand why it works the way it does.

In my context.tsx I wondering why the logged state.value is the static initial value and does not reflect the current value saved therein (see comments in the file below)

import React, { createContext, useState } from "react";

interface myState {
  value: number;
  setValue: Function;
}

export const MyContext = createContext<myState | null>(null);

export const MyContextProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const setValue = (newVal: number) => {
    console.log(state.value); // why is this static?
    setState((prevState) => {
      // prevState does contain the value it currently holds
      return {
        ...prevState,
        value: newVal,
      };
    });
  };

  const [state, setState] = useState({
    value: 10, // whatever is entered here is what will be logged above
    setValue: setValue,
  });

  return <MyContext.Provider value={state}>{children}</MyContext.Provider>;
};

Here's a CodeSandbox with the above file in an app: https://codesandbox.io/p/sandbox/inspiring-pike-9972nf

EDIT

Thanks to https://stackoverflow.com/a/79437349/5086312 I've seen that changing the return value to

  return (
    <MyContext.Provider
      value={{
        ...state,
        // if this line is commented out the console log above
        // will show the initial value, else it shows the current
        setValue: setValue,
      }}
    >
      {children}
    </MyContext.Provider>
  );

Will produce the expected output, however commenting out the indicated line breaks the behaviour. This almost feels like some sort of compiler bug. Notice that having state.value is not actually needed inside the value={...}.


Solution

  • See setValue is being reassigned on every rerender of MyContextProvider, there is no doubt about that.

    But, are you every changing/updating the value of setState in your App component and MyContextProvider? No.

    So basically you are using the same version of setValue as setState throughout the app lifecycle. In MyContextProvider, setValue has closed over setState and state, that is why it is using the old value of state everytime it runs. While it might be using the old value of setState too, that does not matter.

    This is the Sandbox with a fix. It works because now what we are passing down in context is not the original version of setValue but the one that is created on every re-render. (The rerender happens because context is updated)

    Based on the OP's edit: The callback inside setState is a pure function. It takes an input and returns an output, and is not closing over any value.

    (prevState) => {
          console.log({ prevState });
          // prevState does contain the value it currently holds
          return {
            ...prevState,
            value: newVal,
          };
    }
    

    It's like a function like this:

    (a,b) => {
    return a+b; 
    }
    

    You won't expect the above to work incorrectly even if it is stale right.

    So basically, yes similar to state, setState is also outdated above. But it will still function correctly because it is never relying on any outside value.


    Here are some helpful resources:

    1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
    2. When to use functional setState