I have a simple context that sets some value that it get from backend, pseudo code:
export const FooContext = createContext();
export function Foo(props) {
const [value, setValue] = useState(null);
useEffect(() => {
axios.get('/api/get-value').then((res) => {
const data = res.data;
setValue(data);
});
}, []);
return (
<FooContext.Provider value={[value]}>
{props.children}
</FooContext.Provider>
);
}
function App() {
return (
<div className="App">
<Foo>
<SomeView />
</Foo>
</div>
);
}
function SomeView() {
const [value] = useContext(FooContext);
console.log('1. value =', value);
const myFunction = () => {
console.log('2. value = ', value);
}
return (<div>SomeView</div>)
Sometimes I get:
1. value = 'x'
2. value = null
So basically for some reason the value stays as null inside the nested function despite being updated to 'x'
.
This is such a classic stale closure problem. I cannot tell where the closure goes outdated because you didn't show us how you use the myFunction
, but I'm sure that's the cause.
You see, in JS whenever you create a function it will capture inside its closure the surrounding scope, consider it a "snapshot" of states at the point of its creation. value
in the question is one of these states.
But calling myFunction
could happen sometime later, cus you can pass myFunction
around. Let's say, you pass it to setTimeout(myFunction, 1000)
somewhere.
Now before the 1000ms timeout, say the <SomeView />
component has already been re-rendered cus the axios.get
completed, and the value
is updated to 'x'
.
At this point a new version of myFunction
is created, in the closure of which the new value value = 'x'
is captured. But setTimeout
is passed an older version of myFunction
which captures value = null
. After 1000ms, myFunction
is called, and print 2. value = null
. That's what happened.
The best way to properly handle stale closure problem is, like all other programming problems, to have a good understanding of the root cause. Once you're aware of it, code with caution, change the design pattern or whatever. Just avoid the problem in the first place, don't let it happen!
The issue is discussed here, see #16956 on github. In the thread multiple patterns and good practices are suggested.
I don't know the detail of your specific case, so I cannot tell what's the best way to your question. But a very naive strategy is to use object property instead of variable.
function SomeView() {
const [value] = useContext(FooContext);
const ref = useRef({}).current;
ref.value = value;
console.log('1. value =', value);
const myFunction = () => {
console.log('2. value = ', ref.value);
}
return (<div>SomeView</div>)
}
Idea is to depend on a stable reference of object.
ref = useRef({}).current
create a stable reference of same object ref
that don't change across re-render. And you carry it within the closure of myFunction
. It acts like a portal that "teleports" the state update across the boundary of closures.
Now even though stale closure problem still happens, sometimes you might still call outdated version of myFunction
, it's harmless! Cus the old ref
is the same as new ref
, and it's property ref.value
is guaranteed to be up-to-date since you always re-assign it ref.value = value
when re-rendered.
Firstly, you need to provide some default value to your context if no value then set the default value as null.
export const FooContext = createContext(null);
Mostly there is two way for passing the value in the Provider Component. You can pass by an object
or tuple
to value
props in the Provider component.
I'll give you an example by passing an object
in the Provider Component. @dna has given an example of a tuple
.
<FooContext.Provider value={{value,setValue}}>
{props.children}
</FooContext.Provider>
Now if you want to use that value in another component you need to destructure the object like this
const {value, setValue} = useContext(FooContext);
If you have called the nested myFunction()
correctly like shown below then the value would also be x
instead of null.
function SomeView() {
const [value] = useContext(FooContext);
console.log('1. value =', value);
const myFunction = () => {
console.log('2. value = ', value);
}
SomeView.myFunction = myFunction; //updated line
return (<div>SomeView</div>)
}
<button onClick={SomeView.myFunction}>Click</myFunction>
Output :
1. value = 'x'
2. value = ' x'
Now, the question is why it is returning a single character value instead of the state value.
In Javascript, a string is an array of characters. Eg.
const string = ['s','t','r','i','n','g'];
//This is equivalent to
const string = "string";
In your case, your state value may be a string. So, when you destructuring the string you will get the first character of the string.
You will understand more if I give you an example.
const string = "subrato";
const [str] = string;
console.log(str);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With