Search code examples
javascriptreactjsusecallback

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


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

Solution

  • 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)