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.
value
in the question is one of these states.But calling
myFunction
could happen sometime later, cus you can passmyFunction
around. 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.get
completed, and thevalue
is updated to'x'
.At this point a new version of
myFunction
is created, in the closure of which the new valuevalue = 'x'
is captured. ButsetTimeout
is passed an older version ofmyFunction
which capturesvalue = null
. After 1000ms,myFunction
is 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({}).current
create a stable reference of same objectref
that 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 oldref
is the same as newref
, and it's propertyref.value
is guaranteed to be up-to-date since you always re-assign itref.value = value
when re-rendered.