Code:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setTimeout(()=>setNumber(number + 1),2000);
setTimeout(()=>setNumber(number + 1),5000);
}}>+3</button>
</>
)
}
I went through this saying Setting state only changes it for the next render. During the first render, number was 0. This is why, in that render’s onClick handler, the value of number is still 0 even after setNumber(number + 1) was called
and this question already, so some understanding that I have is these calls are async hence the other code continues to run. But my doubt is when will re-rendering happen? For example in my code I know that making change as number=>number+1
will give the desired result but shouldn’t after setNumber(number + 1);
this line re-render should occur but the next two lines are still in time out so they are still waiting, and when the rendering due to setNumber(number + 1);
completes shouldn’t they should see new value of number? What am I missing here?
Now next thing, say these all three lines are queued in some re-rendering queue, there will be re-rendering when the first queue entry resolves and then the next one sees the updated number value, why doesn’t even this happens?
And why does number=>number+1
solves the issue?
2
Answers
When you use asynchronous functions (such as
setTimeout
orPromise
) to trigger state updates, React does not batch them by default. This means that eachsetState
call will cause a separate re-render, and the component will see the stale state value from the previous render. This is why you see only one re-render every click in your code.According to documentation:
So to fix this problem, you need to use a functional update instead of a direct state value. This means that you pass a function to setState that receives the previous state as an argument, and returns the new state based on it. This way, you always use the latest state value, regardless of batching or not. For example:
You can see the whole example here.
number
is a snapshot of what the state was when this component rendered. As aconst
, it can never change. Even if you changed your code tolet
, callingsetNumber
does not even attempt to change your local variables, so it would still not change. So assumingnumber
is currently 0, if you callsetNumber(number + 1)
three times, then you are callingsetNumber(1)
three times. You never callsetNumber(2)
.When you change it to
setNumber(number => number + 1)
, you are no longer using your local variable, so the fact that it never changes doesn’t matter. React knows what the state actually is and will pass it into you. You can then calculate what the new state should be from that and return it. So the first time react passes in 0 and you return 1. Then react passes in 1 and you return 2, and finally react passes in 2 and you return 3.I skipped this question because it kind of doesn’t matter; the issue has entirely to do with stale closure variables, not with timing. But as for when the re-render happens: react waits until the current javascript execution stack finishes, and then if 1 or more states have changed, it does the render. You can think of it as though react is doing a
setTimeout(rerender, 0)
(though i’m not sure that’s actually how it’s implemented — they may be using a microtask)