useState loading not updating before async

Viewed 318

I have a function runValidation that loops through a bunch of numbers and does some calculations. It's might take a while, so I wanted to include an "isLoading" useState.

So I made a simple

const [isLoading, setIsLoading] = useState(false)

When I click a button, I wanna set isLoading to true, run my function and then afterwards do setIsLoading(false). So I made my validation function async, as well as the button handler

const handleClick = async () => {
    setIsLoading(true)
    const isValid = await runValidation()
    setIsLoading(false)
}

And my button component is a simple

<button onClick={() => handleClick()}>Run Validations</button>

Everytime I try to run this however, the loading isn't being set before the validation function is done. So with a useEffect on the "isLoading" variable. I get console.logs showings that

Running Validations
isLoading: true
isLoading: false

Why is my "isLoading" never true until after the async? And is there a way to make sure? I tried moving the setIsLoading(true) down to the button onClick - I tried having .then() instead of await etc. - But isLoading is only set to true after the async function.

EDIT: Example - https://codesandbox.io/s/suspicious-cartwright-0y5jk

1 Answers

Just because you declare a function async doesn't mean it's actually an asynchronous function, it simply allows you to use the await keyword within the function and implicitly return a promise.

In the case of your example codesandbox, the runValidation is declared async but runs completely synchronous code.

const runValidation = async () => {
  let result = 0;
  console.log("Running Validation");
  for (let i = 1; i <= 500000; i++) {
    if (i > 200000) {
      result = i;
      break;
    }
  }

  return result;
};

If you actually do some asynchronously then you'll notice the console logs are identical.

Example:

const runValidation = async () => {
  console.log("Running Validation");

  const result = await new Promise((resolve) => {
    setTimeout(() => {
      let result = 0;
      for (let i = 1; i <= 500000; i++) {
        if (i > 200000) {
          result = i;
          break;
        }
      }
      resolve(result);
    }, 3000);
  });

  return result;
};
Running Validation // logged immediately in callback
isLoading true     // logged in useEffect next render cycle
isLoading false    // logged in useEffect ~3 seconds later

Edit usestate-loading-not-updating-before-async

Why is the log output the same?

const handleClick = async () => {
  setIsLoading(true);
  await runValidation();
  setIsLoading(false);
};

Here you've enqueued a state update and then immediately call runValidation, this will console log "Running Validation" and then "isLoading true" from the useEffect, the implicit Promise is awaited (for not very long) and then another state update is enqueued and "isLoading false" is console logged from the useEffect.

Your code appears to be working exactly as I would expect it to.

If you move the validation log into the asynchronous logic then the console logs output likely more like you were expecting since you're now giving the React state a chance to update and rerender before the "asynchronous" code has finished "firing off".

const runValidation = async () => {
  const result = await new Promise((resolve) => {
    setTimeout(() => {
      console.log("Running Validation");
      let result = 0;
      for (let i = 1; i <= 500000; i++) {
        if (i > 200000) {
          result = i;
          break;
        }
      }
      resolve(result);
    }, 3000);
  });

  return result;
};

Output

isLoading true
Running Validation 
isLoading false
Related