This question already has answers here:
React - useState - why setTimeout function does not have latest state value?
(2 answers)
Closed 2 years ago.
I have a general question about useEffect() vs setTimeout().
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => console.log(count)); // effect
const handleClick = () => {
setCount(count + 1);
setTimeout(() => console.log(count), 5000); // log to console after 5 seconds
}
return (
<>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click Me</button>
</>
);
}
export default Counter;
In the above code,
setCount(count + 1);
setTimeout(() => console.log(count), 5000);
using setTimeout(), I am trying to log count after 5 seconds, by which time setCount(), although asynchronous, would have definitely finished.
Then why does that still print the previous value of count, while useEffect() code prints the updated value? Can someone tell me what I am missing.
A lot of things to say about this code:
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => console.log(count), [count]); // Add the dependencies array, the callback of use effect will be call only if a dependency is updated
const handleClick = () => {
setCount((count) => count + 1); // Use a callback which takes as argument the previous state, because if you click multiple times on the button, as setCount is asynchronous, it won't add 1 for each click
setTimeout(() => console.log(count), 5000); // log to console after 5 seconds
}
return (
<>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click Me</button>
</>
);
}
export default Counter;
I think that it is because the callback of setTimeout is evaluated before the njew count variable is computed, so it logs the previous value, where useEffect's callback is called when a dependency is updated, so when you increment count.
Related
Normally when we need to update a state in a functional component, we do something like this:
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>);
}
When and why will we ever need to use the functional update form?
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>You clicked {count} times</p>
<button onClick={() => setCount(c=>c + 1)}>
Click me
</button>
</div>);
}
Use the function form when the setter may close over an old state value.
For example, if an async request is initiated, and you want to update state after that's done, the request that was made will have scope of the state as it was at the beginning of the request, which may not be the same as the most up-to-date render state.
You may also need to use the function form if the same state value was just updated, eg
setValue(value + 1);
// complicated logic here
if (someCondition) {
setValue(value => value + 1);
}
because the second call of setValue closes over an old value.
State Updates May Be Asynchronous:
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
useState is the same as setState in this condition.
You can see the different when call set state twice:
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
></button>;
<button
onClick={() => {
setCount(c => (c + 1));
setCount(c => (c + 1));
}}
></button>;
There are other use cases too. For example, when you call useState inside an effect. If new state is dependent on old state, this might cause an infinite loop.
useEffect(() => {
setCounter(counter + 1);
}, [counter]);
You can avoid this by using functional updates:
useEffect(() => {
setCounter(old => old + 1);
}, []);
According to this: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
The the functional update form make sure that the previous state that you take reference from is the latest / finalized version when there might be multiple setState hook (which is asynchronous) called (for example, if the user spam click on the button)
And also due to its async nature, the state will not be updated right away within a function, for e.g:
func() {
console.log(counter) // counter = 1
setCounter(counter => counter + 1) // counter = 1
console.log(counter) // counter = 1
}
The functional update form also allows the update function to be passed to its children while still having access to the parent’s state.
function MyButton(props){
// return <button onClick={()=>props.onClick(count+1)}>+1</button>; // error as count is not exposed here
return <button onClick={()=>props.onClick(n=>(n+1))}>+1</button>;
}
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>Counter: {count}</p>
<MyButton onClick={setCount}/>
</div>);
}
ReactDOM.render(<Example/>,document.querySelector("div"));
After clicking on "Click me" button my count value is updating. Now I have to Do Something if count is grater then 0 before my component unmount.
But I have noticed during debugger count value is always 0. While I was expecting count should be grater then 0 if I clicked multiple times.
Please help me, how to get updated value during component unmount. Thanks
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
//ComponentDidMount
return(()=>{
//componentWillUnmount
alert(count); //count 0
if(count){
//Do Something
}
})
},[]);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
You are forming a closure over the original value of count when you set up your cleanup function in the useEffect. That means that even as the value of count updates in the state, the value of count stays as 0 in the cleanup function.
To avoid this, you need to add count to the array of dependencies for your useEffect. That way, when count updates in state, and the component re-renders, the cleanup function also updates with the latest value of count.
useEffect(() => {
return (()=> {
alert(count); // this will now be latest value of count on unmount
if(count) {
// Do Something
}
})
}, [count]); // add is now a dependency of useEffect
The following is a React Hooks experiment of using useState(). It works fine except when the + button was clicked on, then the number can be alternating from 7001 and 7000 and then flashing between some numbers quickly.
Actually, without clicking on the +, the number behaved well but up to about 8000 or 9000, then it might start to flash between some numbers. Why is that and how can it be fixed?
P.S. initial debugging finding was that: it seems Counter() was called multiple times, setting up an Interval Timer every time. So "magically", it seems the useState() ran only once -- for some unknown and magical reason -- or maybe it ran more than once but just returned the exact same content each time, for some magical mechanism. The initial value of 0 really was so for the first time. When it was useState(0) for future times, the count was not 0... we wouldn't want that, but then it wasn't that functional (as in a math function) either.
function Counter() {
const [count, setCount] = React.useState(0);
setInterval(() => {
setCount(count + 1000);
}, 1000);
return (
<div>
<button onClick={() => setCount(count + 1)}> + </button>
{ count }
<button onClick={() => setCount(count - 1)}> - </button>
</div>
);
}
ReactDOM.render(<Counter />, document.querySelector("#root"));
button { margin: 0 1em }
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Code in functional component gets executed every time when component is re-rendered. So, on each re-render you are starting an infinite timer that adds 1000 to counter each second
Each time you change component's state, React re-renders it. Meaning, every execution of setCount leads to new re-render and new timer is started
Also, setCount is asynchronous and if you need to rely on previous state to determine next one, you should call with callback, it like demonstrated in other answer (setCount(c => c + 1))
Something like this is supposed to work:
import React, {useState, useRef, useEffect} from 'react';
function Counter() {
const [count, setCount] = useState(0);
//useRef gives us an object to store things between re-renders
const timer = useRef();
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1000);
}, 1000);
//If we return a function, it will be called when component is dismounted
return () => {
clearInterval(timer.current);
}
}, []);
return (
<div>
<button onClick={() => setCount(count => count + 1)}> + </button>
{ count }
<button onClick={() => setCount(count => count - 1)}> - </button>
</div>
);
}
Not quite sure about the 'why is that' but it's fixed with substituting setCount(c => c + 1) in the buttons and setCount(c => c + 1000) in the interval.
Putting the 'setInterval' in an effect also makes sure that there is only one interval...
React.useEffect(() => {
setInterval(() => {
setCount(c => c + 1000);
}, 1000);
},[])
PS Counter() gets called on every render, I think... while useState only gets called once per mounting by design.
I am running into issues setting a state created with the 'useState' hook from within async functions.
I've created a codepen to demonstrate: https://codepen.io/james-ohalloran/pen/ZdNwWQ
const Counter = () => {
const [count, setCount] = useState(0);
const increase = () => {
setTimeout(() => {
setCount(count + 1);
},1000);
}
const decrease = () => {
setTimeout(() => {
setCount(count - 1);
},1000)
};
return (
<div className="wrapper">
<button onClick={decrease}>-</button>
<span className="count">{count}</span>
<button onClick={increase}>+</button>
</div>
);
};
In the above example, if you click 'increase' followed by 'decrease'..you will end up with -1 (I would expect it to be 0).
If this was a React class instead of a functional component, I would assume the solution would be to use bind(this) on the function, but I didn't expect this to be an issue with arrow functions.
It is because of using setTimeout
Let's assume that you've called the increase() 10 times in a second.
count will be always 0. Because the state is updated after a second, every increment() called in a second will have an unupdated count.
So every increment() will call setCount(0 + 1);.
So no matter how many times you call in a second, the count is always 1.
Ah, I found a solution. I didn't realize I'm able to reference the previousState from the useState setter function: https://reactjs.org/docs/hooks-reference.html#functional-updates
here is my code:
function Tiker() {
var [count, setCount] = useState(0);
useEffect(() => {
var timerID = setInterval(_=>
setCount(count=>count+1)//setCount(count+1) wont work
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, []);
return <div>
this is ticker
<button onClick={() =>
setCount(count + 1)//setCount(count+1) does work
}>up </button>
{count}
</div>
}
By trial and error I discovered that if I use setCount from within setinterval callback, I have to pass a callback to the set state rather than just value.
its not the case if I call from onclick.
Why is that?
The problem is the second argument of useEffect
useEffect(() => {
var timerID = setInterval(_=>
setCount(count=>count+1)//setCount(count+1) wont work
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, []);
It is empty array ([]). It defines list of dependencies for hook. As it empty, it means that hook is not dependent from any value of state or props. So count variable is consumed on first call of useEffect and than stays stale.
To correct this you should either completely remove second argument of useEffect or make array contain [count].
Callback is working as it receives previous count value as first argument.
So correct code will look like
function Tiker() {
var [count, setCount] = useState(0);
useEffect(() => {
var timerID = setInterval(_=>
setCount(count + 1)
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, [count]); // Put variable that useHook depends on
return <div>
this is ticker
<button onClick={() =>
setCount(count + 1) //setCount(count+1) does work
}>up </button>
{count}
</div>
}
It appears that using a callback is the easiest way to change state when called from setInterval. as evident by Frodor comment above and from https://overreacted.io/making-setinterval-declarative-with-react-hooks/