user27985042
user27985042

Reputation: 1

Stale closure in react but I cannot see why

I've got this function in a react component

  const updateDesc = useCallback(
    (text: string) => {
      const tval = text.trim()
      if (tval === '<p></p>' && !view.description?.trim()) return
      if (tval === view.description?.trim() ?? '') return
      const next = {
        ...view,
        description: tval
      }
      onChange?.(next)
    },
    [view]
  )

This is how I call it:

   <MyComponent onBlur={text => updateDesc(text)} />

But the issue is that in updateDesc, the view holds the previous value of view not the latest, and if I had previously changed the title, then change the description, the title reverts to its previous value.

I tried removing useCallback and the issue persisted. This looks strange to me since I would expect the function to have the latest view value, since it's recreated when view changes. I ended up fixing this with a ref but my question remains, why does this happen? Can someone enlighten me please

Upvotes: 0

Views: 40

Answers (1)

WeDoTheBest4You
WeDoTheBest4You

Reputation: 1954

The states after a render is a snapshot. It has the values with respect to the latest render and not with respect to the latest change. Please also be aware, the state updaters in an event will not directly trigger a new render instantly, instead it queues up a render. And the actual render will commence only on finishing the current event. This must be taken note so importantly. Please see below a very simple sample code with test run results. The results show that the state in current render or in the snapshot lags by 1 with the latest value for the queued up render.

App.js

import { useState } from 'react';

export default function App() {
  const [a, setA] = useState(0);

  function handleChange(newValue) {
    console.log(`state during the event : ${a}`);
    setA(newValue);
  }

  return (
    <>
      look at the console for the state value during the render:
      <br></br>
      look here for the state value after the event or render : {a}
      <br />
      <button
        onClick={() => {
          handleChange(a + 1);
        }}
      >
        Change State by 1
      </button>
    </>
  );
}

Test run

a. Browser display on the first click Browser display on the first click

b. Browser display on the second click Browser display on the second click

a. Browser display on the third click Browser display on the third click

Handling a series of state updates in the same event.

we have already discussed above that the state updates in an event will not trigger another render instantly, instead it queues up a new render, and the state updates thus queued up will be processed only on the actual render commenced soon after finishing the current event. Therefore if there is a series of state updates in an event, it must be equipped with updater functions as shown below. The event invokes the state update twice, the second invocation has been equipped with a updater function. It is required to get the latest value from the invocation previous to it. However, the basic principle still holds true that during the render, the state values will be with respect to the snapshot, and it will not be with respect to the latest state updates in the event.

App.js

import { useState } from 'react';

export default function App() {
  const [a, setA] = useState(0);

  function handleChange(newValue) {
    console.log(`state during the event : ${a}`);
    setA(newValue);
  }

  return (
    <>
      look at the console for the state value during the render:
      <br></br>
      look here for the state value after the event or render : {a}
      <br />
      <button
        onClick={() => {
          handleChange(a + 1);
          handleChange((a) => a + 1);
        }}
      >
        Change State by 2
      </button>
    </>
  );
}

Test run

a. Browser display on the first click Browser display on the first click

b. Browser display on the second click Browser display on the second click

c. Browser display on the third click Browser display on the third click

A note on Refs and States

Refs will be retained between renders, however the changes in Refs will not trigger a new render. States on the other hand, will trigger a new render in addition to be retained between renders. Please also take note Ref is an escape hatch from the React world, therefore it may be used as the last resort.

For more on this topic

Citations:

Adding Interactivity

Responding to Events

State: A Component's Memory

Render and Commit

State as a Snapshot

Queueing a Series of State Updates

Upvotes: 0

Related Questions