Home > Enterprise >  setTimeout function in useEffect outputs a cached state value
setTimeout function in useEffect outputs a cached state value

Time:12-25

It's simple. I'm using Redux to manage my state

I have a setTimeout function in a useEffect function.

The setTimeout has a timeout value of 50000ms.

What I Want The SetTimeout Handler To Do

After 50000ms the setTimeout function checks if an api call response has been recieved yet.

If the response hasn't been received yet, the setTimeout function should reinitiate the api call because then the call would have been deemed as timedout.

What The Callback Handler Is Doing

After 50000ms, the setTimeout handler still reinitiates the api call even though the response has been recieved.

I tried logging the output of the state and then it returned a cached state even though the state was passed to the dependency array of the useEffect function and should have been updated

After the api call has been made, the testDetails.isUpdatingTestDetails state is set to false

I tried out several logics and none of them are working

Logic 1

 useEffect(() => {
         //Notice how i check if the testDetails is being updated before initiating the setTimeout callback
        if (testDetails.isUpdatingTestDetails === true) {
         
            setTimeout(() => {
// Inside the settimeout function the same check is also done.
// even though before 50 seconds the response is being received , the function logs the text simulating the reinitiation of an api call
                return testDetails.isUpdatingTestDetails === true &&
                    console.log("After 50 Seconds You Need To Refetch This Data")
            }, 50000);
        }
 

    }, [testDetails.isUpdatingTestDetails, testDetails])

Logic 2

     useEffect(() => {
         setTimeout(() => {
           return testDetails.isUpdatingTestDetails === true &&
             console.log("After 50 Seconds You Need To Refetch This Data")
            }, 50000);
    }, [testDetails.isUpdatingTestDetails, testDetails])

None of the logic i've applied above are working.

CodePudding user response:

Reason for stale state:

The useEffect's callback forms a closure over the state at that time. So, when the timeout's callback gets executed it can only use the old state even if the state is updated in the meantime.

Once the state changes, useEffect will run again (as the state is a dependency) and start a new timeout.

The second timeout will use the new state as the closure is formed with the new state. This timeout is also vulnerable to the stale state issue if the state changes for the third time.


Solution:

You can just clear the previous timeout when the state changes. This way, a timeout's callback won't be executed unless it is the latest.

export default function App() {
  const [state, setState] = useState(true);

  useEffect(() => {
    const timeout = setTimeout(() => {
      console.log(state);
    }, 5000);

    return () => {
      // clears timeout before running the new effect
      clearTimeout(timeout);
    };
  }, [state]);

  return (
    <div className="App">
      <h1>State: {state.toString()}</h1>
      <button onClick={() => setState(false)}>update</button>
    </div>
  );
}

Edit fast-leaf-zk3yo

const { useState, useEffect } = React;

function App() {
  const [state, setState] = useState(true);

  useEffect(() => {
    const timeout = setTimeout(() => {
      console.log(state);
    }, 5000);

    return () => {
      // clears timeout before running the new effect
      clearTimeout(timeout);
    };
  }, [state]);

  return (
    <div className="App">
      <h1>State: {state.toString()}</h1>
      <button onClick={() => setState(false)}>update</button>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>


Alternative solution if you want to run the timeout without increasing the delay. If the state changes after the delay, this will not start a new timeout.

You can use useRef hook to have a reference to the latest state at all times.

Here's an example. You can modify the following to use your variables and logic.

export default function App() {
  const [state, setState] = useState(true);
  const stateRef = useRef(state);

  // this effect doesn't need any dependencies
  useEffect(() => {
    const timeout = setTimeout(() => {
      // use `stateRef.current` to read the latest state instead of `state`
      console.log(stateRef.current);
    }, 5000);

    return () => {
      // just to clear the timeout when component unmounts
      clearTimeout(timeout);
    };
  }, []);

  // this effect updates the ref when state changes
  useEffect(() => {
    stateRef.current = state;
  }, [state]);

  return (
    <div className="App">
      <h1>State: {state.toString()}</h1>
      <button onClick={() => setState(false)}>update</button>
    </div>
  );
}

Edit charming-sinoussi-tmeo2

const { useState, useEffect, useRef } = React;

function App() {
  const [state, setState] = useState(true);
  const stateRef = useRef(state);

  // this effect doesn't need any dependencies
  useEffect(() => {
    const timeout = setTimeout(() => {
      // use `stateRef.current` to read the latest state instead of `state`
      console.log(stateRef.current);
    }, 5000);

    return () => {
      // just to clear the timeout when component unmounts
      clearTimeout(timeout);
    };
  }, []);

  // this effect updates the ref when state changes
  useEffect(() => {
    stateRef.current = state;
  }, [state]);

  return (
    <div className="App">
      <h1>State: {state.toString()}</h1>
      <button onClick={() => setState(false)}>update</button>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>

  • Related