skip to Main Content

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


  1. When you use asynchronous functions (such as setTimeout or Promise) to trigger state updates, React does not batch them by default. This means that each setState 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:

    Setting state does not change the variable in the existing render, but
    it requests a new render. React processes state updates after event
    handlers have finished running. This is called batching. To update
    some state multiple times in one event, you can use setNumber(n => n + 1) updater function.

    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:

    setNumber(number + 1);
    setTimeout(() => {
      console.log(`+2 stale state value: ${number}`);  // log stale state value
      setNumber((number) => number + 1);
    }, 2000);
    setTimeout(() => {
      console.log(`+3 stale state value: ${number}`);  // log stale state value
      setNumber((number) => number + 1);
    }, 5000);
    

    You can see the whole example here.

    Login or Signup to reply.
  2. 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?

    number is a snapshot of what the state was when this component rendered. As a const, it can never change. Even if you changed your code to let, calling setNumber does not even attempt to change your local variables, so it would still not change. So assuming number is currently 0, if you call setNumber(number + 1) three times, then you are calling setNumber(1) three times. You never call setNumber(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.

    But my doubt is when will re-rendering happen?

    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)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search