Reputation: 61
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
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={...}
.
Upvotes: -1
Views: 92
Reputation: 1954
The issue lies here in the below statement.
const [state, setState] = useState({
value: 10,
setValue: setValue,
});
Please recall the initial value passed into useState will be referenced only in the initial render. And thus, in the initial render the state state will become the given initial value. This would be sufficient in most of the cases.
But this initial value will certainly be failed if we reference a state in the closure associated with a function object. Because state will change in every render, but the state in the closure to setValue will remain the old one, the stale one. This is why the reference state.someResult always prints its initial value, although the state is changing in every render.
Solution:
In the scenario we have discussed above, we need to redefine the setValue on each render as you have rightly found as its solution.
As an alternative, we do not reference a state in a closure . As you might have noted, the updater function in React has an argument which will always result the latest state value. Since it is an argument to the function and Not resolved through a closure, it will be free from failure in this context as well.
Therefore please reference the argument instead of the stale closure as shown below.
const setValue = (newVal: number) => {
setState((prevState) => {
console.log(prevState.value); // this will give the latest value
return {
...prevState,
value: newVal,
};
});
};
Upvotes: -1
Reputation: 20626
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:
Upvotes: 1