3

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

Daniel Olsen
  • 1,020
  • 2
  • 15
  • 27
  • Where are you logging the state? State update is async and is constant within a particular render of a component. Component can't see the updated state until it re-renders. – Yousaf Jun 16 '21 at 06:42
  • Can you share a [Minimal, Complete, and Reproducible Code Example](https://stackoverflow.com/help/minimal-reproducible-example) that includes all relevant code? What is `runValidation` doing? Is it also an `async` function or return a Promise? You can't `await` synchronous code. React state updates are also asynchronous. – Drew Reese Jun 16 '21 at 06:44
  • Hi Drew, I added a code sandbox example of the issue. I realize it's probably just my understanding of async and useState that's lacking, but I believe I've done something similar in the past (with API calls and fetch for instance). But this time I made my own async function and something's off... – Daniel Olsen Jun 16 '21 at 06:56

1 Answers1

2

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
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • I see - Thanks. It makes more sense now. Only thing I don't get is that if I make my function with the look asyncronous like you showed (with a new Promise), and useState is asyncronous, shouldn't they be able to run in parallel? So that while the validation function is running, the UI is free to rerender? It seems that, like you said, if I don't put in a timeout, the app won't rerender until my validation is finished. I would have thought that they could run at the same time. – Daniel Olsen Jun 16 '21 at 07:49
  • @DanielOlsen That's exactly what happens, and what I tried showing with the last example. They can't run at the same time though, Javascript is single-threaded. – Drew Reese Jun 16 '21 at 07:50
  • Yes sure - But you put in a 3 second artificial delay right? If the validation is completed very fast, I wouldn't expect to see the loading (or atleast it would flicker on and then off), but if the validation is slow, then it should stay for a bit more. Here you forced atleast 3 seconds delay. Is that necessary just to allow react to rerender? – Daniel Olsen Jun 16 '21 at 07:54
  • @DanielOlsen I think you may misunderstand a bit... the 3s delay was to show that even with *definitely* asynchronous code that the logged output was exactly the same *because* of where the logs occurred. Here's a forked [sandbox](https://codesandbox.io/s/usestate-loading-not-updating-before-async-forked-6mqkj?file=/src/App.js) that includes yours and mine implementations and logs before & after the loop, notice the difference in log output simply because one callback is using asynchronous logic while the other is not. Hopefully this illustrates the difference more clearly. – Drew Reese Jun 16 '21 at 08:06
  • Thank you so much for your patience in explaining this. I do understand about the location of the console logs. You made that very clear with your good examples. Thank you a lot :) Now if I take your async function that you've made in your fork, and remove the "setTimeout", then we're back to the function being finished before "isLoading" - Now is this because the setTimeout is the only way to make sure that my function is run asyncronously? And that if I remove that, it's actually back to running syncronously? (even with the "new Promise")? – Daniel Olsen Jun 16 '21 at 12:02