1

I'm having trouble getting my loading status to appear before the longRunningCode executes. I tried making it async to no avail.

const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);

const longRunningCode = () => {
  const file = // synchronous code to generate a gzipped File object
  return file;
}

// also tried
// const longRunningCode = async () => {
// ...

useEffect(() => {
  if (myAction) {
    setLoadingStatus('Do the thing...')
    const result = longRunningCode()
    // also tried `await` with async version
    // ...
    setLoadingStatus(undefined)
    setMyAction(undefined)
  }
}, [myAction])

//...

return (
  <div>
    <p>{loadingStatus}</p>
    <button onClick={() => setMyAction({})}>Generate file</button>
  </div>
)
Phil
  • 157,677
  • 23
  • 242
  • 245
bgoosman
  • 707
  • 4
  • 13
  • 2
    does `longRunningCode` return a promise? – knicholas Feb 03 '22 at 04:03
  • You cannot use async function as callback for useEffect. I suggest use promises.then.catch.finally or create named async function inside useEffect and call it. – Kirill Skomarovskiy Feb 03 '22 at 04:06
  • I think @knicholas is on the right track. This entirely depends on what `longRunningCode()` does and what it returns. If it's synchronous, we need to know before attempting to answer – Phil Feb 03 '22 at 04:10
  • It is synchronous actually. All I did was add the async keyword to longRunningCode to make it auto wrap (or at least, that's what I thought) everything in a Promise. Am I incorrect in assuming that because longRunningCode has the async keyword, that React will be able to handle the state update and re-render before executing the contents of longRunningCode? – bgoosman Feb 03 '22 at 04:15
  • 1
    No, putting `async` before something doesn't make it asynchronous, it just makes it return a promise and let you use the `await` keyword within. What **exactly** does `longRunningCode()` do (as in [edit your question](https://stackoverflow.com/posts/70965735/edit) and include it)? – Phil Feb 03 '22 at 04:16
  • Of course, I mean, it just seems irrelevant to tell you it's doing all of this ``` const combined = combineParts(header, imageData) const compressed = pako.gzip(combined) const blob = new Blob([compressed], { type: 'application/octet-stream' }) return new File([blob], 'file_name', { type: blob.type }) ``` – bgoosman Feb 03 '22 at 04:21
  • I would if I could. I get the difference between sync and async. I just thought (incorrectly?) that React could sneak in a render between the setLoadingStatus and the longRunningCode. Maybe there's something else which is causing the trouble. – bgoosman Feb 03 '22 at 04:23
  • Does this answer your question? [React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing](https://stackoverflow.com/questions/53332321/react-hook-warnings-for-async-function-in-useeffect-useeffect-function-must-ret) – Benjamin Feb 03 '22 at 04:34
  • 1
    It's not React that is doing that, it's JavaScript itself. It's called Event Loop, all async code goes into the event loop and it frees the way for sync code to run. That is why your code is running before the loading state is updated. What you have to do it either wrap the sync code in a promise or use setTimeout to wait for the state to be updated and then run the sync code. – Windbox Feb 03 '22 at 04:45

2 Answers2

4

I was thinking something along the lines of:

const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);
const longRunningCode = () => {
 return new Promise(resolve, reject)=>{
  const file = // synchronous code to generate a File object
  resolve(file);
 }
}

useEffect(() => {
  if (myAction) {
    setLoadingStatus('Do the thing...')
    longRunningCode().then(file=>{
      // ...
      setLoadingStatus(undefined)
      setMyAction(undefined)
    })
  }
}, [myAction])

//...

return <p>{loadingStatus}</p><button onClick={() => setMyAction({})} />

**Edit: ** with setTimeout

const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);
const longRunningCode = () => {
  const file = // synchronous code to generate a File object
  return file
 }
}

useEffect(() => {
  if (myAction) {
    setLoadingStatus('Do the thing...')
     
     //a 0 second delay timer waiting for longrunningcode to finish
     let timer = setTimeout(() =>{
      longRunningCode()
      setLoadingStatus(undefined)
      setMyAction(undefined)
     }, 0);
      // clear Timmer on unmount
      return () => {
        clearTimeout(timer);
      };
    
  }
}, [myAction])

//...

return <p>{loadingStatus}</p><button onClick={() => setMyAction({})} />
knicholas
  • 520
  • 4
  • 10
  • 2
    This might work by queuing the synchronous work in a microtask. There's a chance it could still block React's render though given state setters are also async. `setTimeout` might be the only option in that case – Phil Feb 03 '22 at 04:36
  • Thank you for the suggestion knicholas! I tried it without any luck. I'm curious about what Phil said about state settings being async. Should I just RTFReactM? :) – bgoosman Feb 03 '22 at 04:53
  • "queueing the synchronous work in a microtask" is what I thought I was doing by wrapping my code in async. So I think understanding how React state setters work in depth will really help. – bgoosman Feb 03 '22 at 04:57
  • Actually setTimeout did the trick (just tried it). Though I wish I didn't have to resort to it – bgoosman Feb 03 '22 at 05:07
  • @bgoosman making the browser do a bunch of compression on the main thread is less than ideal. – Phil Feb 03 '22 at 05:09
  • That's absolutely right. Maybe I should use a web worker – bgoosman Feb 03 '22 at 05:18
  • But I don't mind so much blocking the main thread for this particular thing. I just wish there were a way I could prioritize the state update / render somehow. – bgoosman Feb 03 '22 at 05:19
  • I guess setTimeout is sort of like that. Maybe I should wrap it :D `const deprioritize = (func) => setTimeout(func)` – bgoosman Feb 03 '22 at 05:21
  • 1
    I doubt OP wants to wait 3 seconds before starting the task. It just needs a long enough delay to enable the `setLoadingStatus('Do the thing...')` to apply. Even `0` might be enough – Phil Feb 03 '22 at 05:44
  • 1
    Yeah, 0 is fine. My code is essentially this now: `doReactStateUpdate(); setTimeout(longRunningCode);`. – bgoosman Feb 03 '22 at 05:56
  • 1
    React’s setState has a callback option, so refactoring my FC into a class to use setState may also be an option – bgoosman Feb 03 '22 at 13:44
  • I ended up using setTimeout to good effect – bgoosman Jul 17 '22 at 23:24
-1

useEffect callbacks are only allowed to return undefined or a destructor function. In your example, you have passed an async function which returns a Promise. It may or may not run, but you will see React warnings that useEffect callbacks are run synchronously.

Instead, define an async function, and then call it.

const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);

useEffect(() => {
  const doAction = async () => {
    if (myAction) {
      setLoadingStatus('Do the thing...');
      const result = await longRunningCode();
      // ...
      setLoadingStatus(undefined);
      setMyAction(undefined);
    }
  };

  doAction();
}, [myAction]);

//...

return <p>{loadingStatus}</p><button onClick={() => setMyAction({})} />

Otherwise, what you have written will work.

Benjamin
  • 3,428
  • 1
  • 15
  • 25
  • This does not answer the question at all – Phil Feb 03 '22 at 04:08
  • @Phil, op asked how to do updates in an async useEffect callback. I answered his question appropriately. – Benjamin Feb 03 '22 at 04:10
  • What OP has already works, regardless of warnings. See the repro link above in the comments – Phil Feb 03 '22 at 04:15
  • @Phil obviously it works. But the answer to the question "How do you update state between long running code?" is: define an async function, and call it inside a useEffect callback. That is what I have written above. – Benjamin Feb 03 '22 at 04:26
  • OP's problem is that `longRunningCode` is synchronous and blocks the main thread, preventing React's re-render from firing. Like I said, this does not answer the question – Phil Feb 03 '22 at 04:28
  • Okay @Phil, well that is new information, and now the context of the question has changed. – Benjamin Feb 03 '22 at 04:31
  • You'd be amazed how often that happens around here. FYI - here's a good canonical answer to use as a duplicate target for any `useEffect(async ...` problems ~ [React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing](https://stackoverflow.com/a/53572588/283366) – Phil Feb 03 '22 at 04:33