Home > Blockchain >  Why does this React setter work if it should be a stale closure?
Why does this React setter work if it should be a stale closure?

Time:01-26

I have this below function. My randomize function is the same across renders, as I have wrapped it in a useCallback. When I click the randomize button, it re-renders my app.

However, when I click that button, since randomize is memoized, don't I use the old setNum function? How does this work? Aren't the setter functions linked to their respective states, so the stale setter function would be changing an oudated state? Is it best practice to include the setter a dependency? And what practical difference does it make since the code seems to work as is?

export default function App() {
  const [num, setNum] = useState(0);

  const randomize = useCallback(() => {
    setNum(Math.random());
  }, []);

  return (
    <div className="App">
      <h4>{num}</h4>
      <button onClick={randomize}>Randomize</button>
    </div>
  );
}

CodePudding user response:

There are no stateful values referenced inside the useCallback, so there's no stale state that could cause issues.

Additionally, state setters are stable references - it's the exact same function across all renders. (See below for an example.) Each different setNum is not tied only to its own render - you can call any reference to it at any time, and the component will then re-render.

let lastFn;
const App = () => {
    const [value, setValue] = React.useState(0);
    if (lastFn) {
      console.log('Re-render. Setter is equal to previous setter:', lastFn === setValue);
    }
    lastFn = setValue;
    setTimeout(() => {
      setValue(value   1);
    }, 1000);
    return (
      <div>
        {value}
      </div>
    );
};

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

Is it best practice to include the setter a dependency?

In general, yes, it's a good idea to include as a dependency everything that's being referenced inside - but ESLint's rules of hooks is intelligent enough to recognize that the function returned by useState is stable, and thus doesn't need to be included in the dependency array. (Pretty much anything else from props or state should be included in the dependency array though, and exhaustive-deps will warn you when there's something missing)

CodePudding user response:

Aren't the setter functions linked to their respective states, so the stale setter function would be changing an outdated state?

No, because it will never be stale.

From the docs: Hooks API Reference > Basic Hooks > useState:

Note

React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.


Is it best practice to include the setter a dependency?

Technically, it's a deoptimization that will have an imperceptible runtime cost. If it gives you confidence about following the dependency list rules, then add it to the list.

CodePudding user response:

Here is an illustration of the "stale closure" issue and how React addresses it:

    import { useState, useCallback } from 'react';
    
    function Parent() {
      const [count, setCount] = useState(0);
    
      const handleClick = useCallback(() => {
        console.log('count:', count);
        setCount(count   1);
      }, [count]);
    
      return (
        <>
          <Child handleClick={handleClick} />
        </>
      );
    }
    
    function Child({ handleClick }) {
      const [name, setName] = useState('John');
    
      return (
        <>
          <button onClick={handleClick}>Increment</button>
          <input value={name} onChange={e => setName(e.target.value)} />
        </>
      );
    }

The handleClick is formed and the count is 0 when the Parent component renders for the first time. The handleClick method is provided by the Parent component to the Child component, which also renders for the first time.

The handleClick function will now be called when the button is clicked, printing "count: 0" to the console and increasing the count at the same time.

However, the Child component re-renders and handleClick is constructed with the count set to 1 when the value of the input is changed. Because the handleClick method is using the outdated value of count, if you click the button again, "count: 0" rather than the anticipated "count: 1" will be printed to the console.

React solves this issue by establishing a new function each time a component re-renders, ensuring that it always uses the most recent state.

Because the handleClick method is not passed on to another component in the aforementioned example, the useCallback hook is not required, but it is used to demonstrate the issue and how React handles it.

Generally speaking, this issue can be avoided by adding the state and other dependencies to the useCallback hook's dependency list, which will cause the function to be recreated if one of the dependencies changes.

  • Related