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'.
Explanation
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.
valuein the question is one of these states.But calling
myFunctioncould happen sometime later, cus you can passmyFunctionaround. Let's say, you pass it tosetTimeout(myFunction, 1000)somewhere.Now before the 1000ms timeout, say the
<SomeView />component has already been re-rendered cus theaxios.getcompleted, and thevalueis updated to'x'.At this point a new version of
myFunctionis created, in the closure of which the new valuevalue = 'x'is captured. ButsetTimeoutis passed an older version ofmyFunctionwhich capturesvalue = null. After 1000ms,myFunctionis called, and print2. value = null. That's what happened.Solution
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.
Idea is to depend on a stable reference of object.
ref = useRef({}).currentcreate a stable reference of same objectrefthat don't change across re-render. And you carry it within the closure ofmyFunction. 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 oldrefis the same as newref, and it's propertyref.valueis guaranteed to be up-to-date since you always re-assign itref.value = valuewhen re-rendered.