简体   繁体   中英

Cannot retrieve current state inside async function in React.js

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!

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]);

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 for all those timers? 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

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;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM