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 50000
ms.
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>
);
}
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>
);
}
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>