0

I have created some state at the top level component (App), however it seems that when this state is updated, the updated state is not read by the asynchronous function defined in useEffect() (it still uses the previous value), more detail below:

I am attempting to retrieve the state of the const processing in the async function toggleProcessing defined in useEffect(), so that when processing becomes false, the async function exits from the while loop. However, it seems that when the processing updates to false, the while loop still keeps executing.

The behaviour should be as follows: Pressing the 'Begin Processing' button should console log "Processing..." every two seconds, and when that same button is pressed again (now labeled 'Stop Processing'), then "Stopping Processing" should be console logged. However, in practice, "Stopping Processing" is never console logged, and "Processing" is continuously logged forever.

Below is the code:

import React, { useState, useEffect} from 'react'

const App = () => {

    const [processing, setProcessing] = useState(false)

    const sleep = (ms) => {
        return new Promise(resolve => setTimeout(resolve, ms))
    }

    useEffect(() => {
        const toggleProcessing = async () => {
            while (processing) {
                console.log('Processing...')
                await sleep(2000);
            }
            console.log('Stopping Processing')
        }
        if (processing) {
            toggleProcessing()      // async function
        }
    }, [processing])

    return (
        <>
            <button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
        </>
    )
}

export default App;

It really just comes down to being able to read the updated state of processing in the async function, but I have not figure out a way to do this, despite reading similar posts.

Thank you in advance!

nzy
  • 3
  • 3

3 Answers3

0

If you wish to access a state when using timeouts, it's best to keep a reference to that variable. You can achieve this using the useRef hook. Simply add a ref with the processing value and remember to update it.

const [processing, setProcessing] = useState<boolean>(false);
const processingRef = useRef(null);

useEffect(() => {
    processingRef.current = processing;
}, [processing]);
saguirrews
  • 280
  • 1
  • 7
0

I was interested in how this works and exactly what your final solution was based on the accepted answer. I threw together a solution based on Dan Abramov's useInterval hook and figured this along with a link to some related resources might be useful to others.

I'm curious, is there any specific reason you decided to use setTimeout and introduce async/await and while loop rather than use setInterval? I wonder the implications. Will you handle clearTimeout on clean up in the effect if a timer is still running on unmount? What did your final solution look like?

Demo/Solution with useInterval

https://codesandbox.io/s/useinterval-example-processing-ik61ho

import React, { useState, useEffect, useRef } from "react";

const App = () => {
  const [processing, setProcessing] = useState(false);

  useInterval(() => console.log("processing"), 2000, processing);

  return (
    <div>
      <button onClick={() => setProcessing((prev) => !prev)}>
        {processing ? "Stop Processing" : "Begin Processing"}
      </button>
    </div>
  );
};

function useInterval(callback, delay, processing) {
  const callbackRef = useRef();

  // Remember the latest callback.
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      callbackRef.current();
    }
    if (delay !== null && processing) {
      let id = setInterval(tick, delay);

      console.log(`begin processing and timer with ID ${id} running...`);

      // Clear timer on clean up.
      return () => {
        console.log(`clearing timer with ID ${id}`);
        console.log("stopped");
        clearInterval(id);
      };
    }
  }, [delay, processing]);
}

export default App;

Relevant Links

Dan Abramov - Making setInterval Declarative with React Hooks

SO Question: React hooks - right way to clear timeouts and intervals

setTimeout and clearTimeout in React with Hooks (avoiding memory leaks by clearing timers on clean up in effects)

abgregs
  • 1,170
  • 1
  • 9
  • 17
  • To be honest I did not know about `setTimeout` as I am quite new to React. Please find the code that ended up working for me in the answer I have put in this comment section. – nzy Jan 28 '23 at 13:33
0

Here is the working code:

import React, { useState, useEffect, useRef} from 'react'

const App = () => {

    const [processing, setProcessing] = useState(false)
    const processingRef = useRef(null);

    const sleep = (ms) => {
        return new Promise(resolve => setTimeout(resolve, ms))
    }

    useEffect(() => {
        const toggleProcessing = async () => {
            while (processingRef.current) {
                console.log('Processing')
                await sleep(2000);
            }
            console.log('Stopping Processing')
        }
        processingRef.current = processing;
        if (processing) {
            toggleProcessing()      // async function
        }
    }, [processing])

    return (
        <>
            <button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
        </>
    )
}

export default App;
nzy
  • 3
  • 3