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

69 Views Asked by At

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>
  );
}
2

There are 2 best solutions below

7
CertainPerformance On BEST ANSWER

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)

0
jsejcksn On

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.