31

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    return (setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000))
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

I just want to know if this works the way that I think it does, or if there is a better explanation!

Whenever the setState method is called, the state gets a new reference. This means the original state doesn't have a new value, but instead creates a new state with a new value. When we click on the second button, the event handler function captures the reference of the original state.

Even if we click the first button many times, when the alert is displayed it will show the value of the state that the event handler captured its reference.

Is this correct?

Community
  • 1
  • 1
A2ub
  • 374
  • 1
  • 3
  • 9
  • What issue are you getting? – Tushar Jul 09 '20 at 02:55
  • i just asked if what i had inderstood is correct !! Whenever the setState method is called the state gets a new reference. This means the original state doesn't have a new value, but we instead create a new state with a new value. When we click on the second button, the event handler function captures the reference of the original state. Even if we click the first button many times, when the alert is displayed it will show the value of the state that the event handler captured its reference – A2ub Jul 09 '20 at 04:47
  • what's the problem doing what you mentioned here? I don't see any issue, clicking it 5 itmes, then clicking the second, alert message says "5 times clicked". No problem. – Dawei Sep 08 '22 at 22:04

2 Answers2

44

The reason the alert shows the outdated value of count is because the callback passed to setTimeout is referencing an outdated value of count captured by the closure. This is usually referred to as a stale-closure.

On the initial render, the anonymous function passed as a callback to setTimeout captures the value of count as 0, and when the button show alert gets clicked the callback gets queued but with the outdated value of count.

In the case above the easiest solution to show the updated value of count in the alert message and fix the stale-closure issue will be to use a ref.

function App() {
  const [count, setCount] = useState(0);

  const latestValue = useRef(count);

  const handleAlertClick = () => {
    setTimeout(() => {
      alert(`count is: ${latestValue.current}`);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          setCount(prev => {
            latestValue.current = prev + 1;
            return prev + 1;
          });
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

Working demo in codesandbox

Hooks rely heavily on closures to work, so it very likely that you may bump into problems regarding stale-closures. Here is a nice article on how stale-closures create issues when using react-hooks and demonstrates how to fix some the issue in some situations.

subashMahapatra
  • 6,339
  • 1
  • 20
  • 26
  • I Don't Think it's About Capturing The value of count, instead it's about capturing the reference of the count variable, do you agree with me ? please read what i said on top below the code in my question ! – A2ub Jul 09 '20 at 04:46
  • It has nothing to do with reference to the state. The behaviour is entirely related to how closure works and how closures can capture an outdated variable. The issue can easily be reproduced in a function. This is not limited to react. See the first example provided in the [article](https://dmitripavlutin.com/react-hooks-stale-closures/) – subashMahapatra Jul 09 '20 at 05:28
  • it's not about capturing outdated variable only, if this the case why the value of value variable steal updated ? it's all about reference the issue is whenever we call the inc() function the message variable initialize with a new adress in the memory, that mean the closure capture the reference of the first message that's why when we call the log function it return the value 1. and if we tried to initialize the message variable on the top with the value variable that will solve the problem because we will not need to initialize the message variable any time we call the inc function !! – A2ub Jul 09 '20 at 05:48
  • Ok i just realized the point that we differ on it when i say the reference i mean that the closure capture the refrence of the variable to return to it whenever it want to use the value of that reference ! what we deal about it is closure doesn't capture the value instead it capture the variable (wich i mean in my case the refrence) !! – A2ub Jul 09 '20 at 06:32
  • Note that `useRef` creates an object that will **persist for the full lifetime of the component** and that React will give you the **same ref object on every render**. This works because by referencing the same object in memory any callback, regardless of its scope when defined or executed, is pointing to the same object which eliminates the stale reference. – jhovanec Feb 19 '21 at 17:17
  • @A2ub I think your assumption seems right. Have you found out the answer to this? I am keen to know that. – Shawn Feb 24 '22 at 05:35
  • 1
    @A2ub Yes. You are correct. This is because of the immutable approach to state React uses. With immutable state, each new value of the `count` variable is a whole new object (a new reference) and closures are bundled with references to their lexical scope. Thus, in react, if you reference a state variable from a closure, you end up with stale closures. UseRef gets around this, because UseRef gives you a reference that stays consistent for the lifecycle of the component is and is mutable. Using the dependencies array also works, because it reruns the effect closure each time state changes – Daniel Bingham Jul 15 '22 at 17:36
  • @subashMahapatra I'm tempted to downvote you, because you failed to actually answer the question in either your answer or the article you linked. You showed how to get around the issue in both, which is what most people are looking for when they come here, but didn't explain why react behaves differently than standard javascript closures. In the first example linked in your article, one would expect `count` to behave like `value` (and thus, not be stale) rather than like `message`. It behaves like `message` because of React's immutable state. I won't downvote, but I encourage an edit. – Daniel Bingham Jul 15 '22 at 17:40
  • Actually without the using useRef, without doing anything, the original version worked fine. Not sure what we are trying to solve here https://codesandbox.io/s/cold-monad-8z2pj8 – Dawei Sep 20 '22 at 16:34
1

https://dmitripavlutin.com/react-hooks-stale-closures/#32-usestate

  function App() {
  const [count, setCount] = useState(0);

  const handleAlertClick = () => {
    setTimeout(() => {
      alert(`count is: ${count}`);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          setCount((count) => count + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}