1

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???

3 Answers3

6

The 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>
    </>
  );
}
Erick
  • 1,098
  • 10
  • 21
  • 1
    Can you explain why even if you put this inside the `addOne` function `setTimeout(() => console.log(count), 1000);` It will still print the old value? I mean shouldn't the timeout allow time for the `count` var to update? Because that's precisely what the OP wants to know. – codemonkey Feb 04 '21 at 04:42
  • @codemonkey The explaination is giving here: https://stackoverflow.com/questions/55198517/react-usestate-why-settimeout-function-does-not-have-latest-state-value – Erick Feb 04 '21 at 10:06
3

Closures

You are experiencing the unupdated state in the console log, because of closures.

when your function is created when the component 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 component 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 component

 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 combined with the asynchronous behavior of setState

Async behaviour

codesandbox

Async behaviour becomes 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.

GBourke
  • 1,834
  • 1
  • 7
  • 14
  • 1
    added an explanation and codesandbox for async timeouts, with setState functions, and the importance thereof. – GBourke Feb 04 '21 at 05:14
1

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 component re-renders a new closure is created but that doesn't change the value that was initially closed over.

Source: https://github.com/facebook/react/issues/14010#issuecomment-433788147

So there you have it.

codemonkey
  • 7,325
  • 5
  • 22
  • 36