1

I am aware of the issue of stale state in nested functions or callbacks (e.g. setTimeout) inside a React component. For example, in the code below (take from React docs) we see the issue of stale state:

"If you first click “Show alert” and then increment the counter, the alert will show the count variable at the time you clicked the “Show alert” button."

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

  function handleAlertClick() {
    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 know that we can create a mutable reference to the most up to date/current value of state by using useRef:

function Example() {
  const [count, setCount] = useState(0);
  const stateRef = useRef()
  stateRef.current = count
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + stateRef.current);
    }, 3000);
  }

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

My question is, why does creating a reference to our useRef variable (stateRef) solve the problem of stale state, and why does this get around the issue of closure? Surely by just referencing count directly inside setTimeout, once the component re-renders the count value will have updated, and therefore our reference to it would return the up to date value.

I am struggling to understand why referencing stateRef as opposed to count gets around the issue of stale state, given they are both declared in the same lexical scope from setTimeouts point of view.

Sean
  • 587
  • 4
  • 20
  • 1
    Since ref is just an object, there is no closure on **object's values**, its not related to React, just Javascript, reading about closures should explain it https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures – Dennis Vash Mar 14 '21 at 09:55

2 Answers2

7

Try imagine a timeline, each re-render of your component creates a "snapshot" and leaves a mark on this timeline.

So called "stale state" is more often referred as "stale closure", which more accurately describes the real problem.

How precisely closure causes trouble?

Each time a component re-render, the component function itself is re-executed from start to end, and during this execution:

  1. all the hook calls along the way are called
  2. all the local variables are re-initialzed
  3. and all the nested functions inside the component function are re-declared
  4. and MOST IMPORTANTLY, if any of these nested functions reference the local variables within the component function, such reference is really pointing to a "closure", or a "snapshot" that captures the current states of those local variables, created on-the-fly and stored somewhere inside memory.

If you call setTimeout and send a callback function to it, and that function references a local variable, it's really stored inside one of these snapshots on the rendering timeline.

As the component re-renders, new snapshots are added to the timeline and only the latest one is up-to-date, while as the callback function is still referencing a stale snapshot/closure.

By the time setTimeout decide to execute the callback, the callback look at its own version of snapshot/closure and say "OK, the 'count' variable is 0" unaware of it's stale.

How useRef solves the problem?

Cus const ref = useRef() always returns the same object reference across re-rendering. So even if callback look at a stale snapshot/closure, it still sees the same object ref as in the latest snapshot. And since each re-execution of the component function always sets the ref.value = someValue property with latest value, the callback got a way to access the latest value.

hackape
  • 18,643
  • 2
  • 29
  • 57
1

Surely by just referencing count directly inside setTimeout, once the component re-renders the count value will have updated, and therefore our reference to it would return the up to date value.

No, when you clicked on the button, the function useSetTimeout used the callback at hand at that time. And count is NOT a reference to your variable. When the component rerenders, the callback won’t know you changed the value at all.

useRef is different in this that it returns an object and objects work a bit differently than other types of variables. And since it’s always the same object, you’ll always deal with the same variable.

By the way, when you call setCount, be sure to always do setCount(prevState => ...). Otherwise you’re not guaranteed to have the latest value.

jperl
  • 4,662
  • 2
  • 16
  • 29