After clicking the button the console shows 0 and the page 1
function App() {
const [count, setCount] = useState(0);
const addOne = () => {
setCount(count + 1)
console.log(count)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
I think is because the setCount()
is happening asynchronously but even if I add a setTimeout to the console.log()
, the console keeps showing the unupdated state
Why???
After clicking the button the console shows 0 and the page 1
function App() {
const [count, setCount] = useState(0);
const addOne = () => {
setCount(count + 1)
console.log(count)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
I think is because the setCount()
is happening asynchronously but even if I add a setTimeout to the console.log()
, the console keeps showing the unupdated state
Why???
Share Improve this question asked Feb 4, 2021 at 4:13 GoodLookingCoderGoodLookingCoder 354 bronze badges 1- State changes are asynchronous – Jayce444 Commented Feb 4, 2021 at 4:15
3 Answers
Reset to default 6The state updation in React is always asynchronous. you will find the updated state value of count in useEffect
function App() {
const [count, setCount] = useState(0);
useEffect(()=> {
console.log('count',count);
},[count])
const addOne = () => {
setCount(count + 1)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
Closures
You are experiencing the unupdated state in the console log, because of closures.
when your function is created when the ponent is rendered, and closure is created with the value of count at the time the closure is created.
if the value of count is 0, and your ponent rerenders, a closure of your function will be created and attached to the event listener of the onlcick.
in that case, the first render of your ponent
const addOne = () => {
setCount(count + 1)
console.log(count)
}
is equivalent to (replace count with 0)
const addOne = () => {
setCount(0 + 1)
console.log(0)
}
therefore it makes sense in your case that count is 0 when it is console logged.
In this case, I believe its the closure you are experiencing bined with the asynchronous behavior of setState
Async behaviour
codesandbox
Async behaviour bees a problem when asynchronous actions are occuring. setTimeout is one of the basic async actions. Async actions always require that you provide a function to the setCount function, which will accept the latest state as a parameter, with the nextState being the return value of this function. This will always ensure the current state is used to calculate the next state, regardless of when it is executed asynchronously.
const addOneAsync = () => {
setCountAsync((currentState) => {
const nextState = currentState + 1;
console.log(`nextState async ${nextState}`);
return nextState;
});
};
I have created a codesandbox demonstrating the importance of this. CLick the "Count" button fast 4 times. (or any number of times) and watch how the count result is incorrect, where the countAsync result is correct.
addOneAsync:
when the button is clicked, a closure is created around addOneAsync
, but since we are using a function which accepts the currentState, when it eventually fires, the current state will be used to calculate the next state
addOne:
When the button is clicked, a closure is created around addOne
where count is captured as the value at the time of the click. If you click the count button 4 times before count has increased, you will have 4 closures of addOne set to be fired, where count is captured as 0.
All 4 timeouts will fire and simply set count to 0 + 1, hence the result of 1 for the count.
Yes, you're right about the origins of this behavior and the other posters here seem to have explained how to fix it. However, I don't see the answer to your specific question:
...but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state Why???
So what you mean is that even if you handle that console.log call like so:
const addOne = () => {
setCount((count) => count + 1);
setTimeout(() => console.log(count), 1000);
}
It will STILL print the old, un-updated value of count
. Why? Shouldn't the timeout allow time for count
to update? I will quote the answer:
This is subtle but expected behavior. When setTimeout is scheduled it's using the value of
count
at the time it was scheduled. It's relying on a closure to access count asynchronously. When the ponent re-renders a new closure is created but that doesn't change the value that was initially closed over.
Source: https://github./facebook/react/issues/14010#issuement-433788147
So there you have it.