While writing a React app, I run in a problem that can be simplified like this:
const [prjToFetch, setPrjToFetch] = React.useState([]);
React.useEffect(() => {
console.log("Before " + prjToFetch)
async function func(){
let myPromise = await new Promise(function(myResolve, myReject) {
setTimeout(function() { myResolve("I love You !!"); }, 3000);
});
console.log("After: " + prjToFetch)
return myPromise;
}
func()
}, [prjToFetch])
I have a button that pushes the value "a" into the array prjFetch whenever it is pressed:
const handleClick = () => {
setPrjToFetch([...prjToFetch, "a"]);
}
If I press the button once, the output of the previous code is:
Before a
(3 seconds of idle time)
After a
This is exactly what I expect. But let’s suppose I press the button twice in a row, then the output is:
Before a
Before a a
(3 second of idle time)
After a
After a a
Why is the first "After" returning only "a" and not "a a"?
Notice that I also tried to rewrite the code as:
React.useEffect(() => {
console.log("Before " + prjToFetch)
async function func(){
await new Promise(function(myResolve, myReject) {
setTimeout(function() { myResolve("I love You !!"); }, 3000);
});
}
func().then(() => {
console.log("After: " + prjToFetch)
})
}, [prjToFetch])
But the result is the same
The problem is that
func
closes over the value ofprjToFetch
as of whenfunc
was created, and you’re using that value asynchronously. By the timefunc
uses the value, it’s stale.Here’s what’s happening:
useEffect
callback.func
that closes over the currentprjToFetch
variable.func
func
starts waiting for three secondsprjToFetch
‘s state setterprjToFetch
has changed, React calls youruseEffect
callback againfunc
that closes over the newprjToFetch
variablefunc
func
starts waiting three secondsfunc
finishes waiting, and shows the value of theprjToFetch
that it closes overfunc
finishes waiting, and shows the value of theprjToFetch
that it closes over (which is not the same as the one the previousfunc
closed over)It’s exactly the same as this:
You could return a function from your
useEffect
callback so it knows that the component has been re-rendered between steps 2 and 3 above.The real solution will vary a fair bit based on what you actually want to do. But if you want
func
to always see the then-current value ofprjToFetch
, and you want bothfunc
instances to run, you need to use thesetPrjToFetch
setter to access the current value (which is a bit hacky):Again, that’s a bit hacky, and usually there are better solutions that are more specific to what you’re actually using
prjToFetch
for.I suggest reading A Complete Guide to
useEffect
by Dan Abramov. It helps you understanduseEffect
, but also hooks in general.