Asynchronous function keeps reading old value of global variab;e

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

1 thought on “Asynchronous function keeps reading old value of global variab;e”

  1. The problem is that func closes over the value of prjToFetch as of when func was created, and you’re using that value asynchronously. By the time func uses the value, it’s stale.

    Here’s what’s happening:

    1. The component mounts, triggering a call to your useEffect callback.
      1. That creates a copy of func that closes over the current prjToFetch variable.
      2. It calls that func
      3. That func starts waiting for three seconds
    2. You click the button, triggering prjToFetch‘s state setter
    3. That makes the component function run again, creating a new set of state variables. Since prjToFetch has changed, React calls your useEffect callback again
      1. That creates a new func that closes over the new prjToFetch variable
      2. It calls that func
      3. That func starts waiting three seconds
    4. The first func finishes waiting, and shows the value of the prjToFetch that it closes over
    5. The second func finishes waiting, and shows the value of the prjToFetch that it closes over (which is not the same as the one the previous func closed over)

    It’s exactly the same as this:

    let aInComponentState = 1;
    function example() {
        const a = aInComponentState;
        console.log(`before: ${a}`);
        setTimeout(() => {
            console.log(`after: ${a}`);
        }, 1000);
    }
    
    example();           // component mounts
    ++aInComponentState; // button click
    example();           // component function called again

    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 of prjToFetch, and you want both func instances to run, you need to use the setPrjToFetch setter to access the current value (which is a bit hacky):

    setPrjToFetch(currentPrjToFetch => {
        console.log("After: " + currentPrjToFetch);
    });
    

    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 understand useEffect, but also hooks in general.

    Reply

Leave a Comment