2

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>
  );
}
713sean
  • 313
  • 11

2 Answers2

1

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)

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • If it's the same across renders, is it best practice to add the setter as defensive programming, in the case things change in the future? – 713sean Jan 26 '23 at 04:52
  • 1
    I can't imagine that changing. It would be backwards-incompatible (which is a major no-no in many cases unless there are benefits in exchange) and would require all React codebases to add some boilerplate. Better to write whatever concise code that works for the current version of React, I think - if a new version eventually comes out that changes things, it should be something to do in the migration process, rather than writing unnecessary code before then. – CertainPerformance Jan 26 '23 at 04:56
  • That makes sense. A few more follow ups. 1) Is the only reason to include setters as dependencies essentially to instill confidence in the code? 2) If I have a function I import inside my module, and I use that in my function inside useCallback, I would not add this to the dependencies, as when I re-render I don't change that imported function? 3) How does your code snippet work, because every time the app re-renders, doesn't lastFn become undefined again? – 713sean Jan 26 '23 at 05:16
  • 1
    (1) It's unnecessary. One could include it if they felt like like it, but it doesn't accomplish anything (and won't for the forseeable future), so whether you go one way or the other is more of a stylistic choice than anything else. (2) If the function is imported, and isn't from state, props, or context, then it's not a React function, so it doesn't really have a concept of being stale in the first place. An imported function shouldn't ever change its identity anyway, IMO. – CertainPerformance Jan 26 '23 at 05:28
  • (3) `lastFn` is an identifier that's initialized once, when the page first loads - see how it's declared at the top, and not inside the component. On mount and re-render, a new value is assigned to the identifier. There's no `lastFn = undefined` anywhere; once a function is assigned, it never holds `undefined` again. – CertainPerformance Jan 26 '23 at 05:28
  • 1 & 2 make sense, thank you. To my understanding for #3, I get that `lastFn` is held outside the component. But wouldn't on every re-mount / re-render the component capture `lastFn` (which is the same `lastFn` each time) within their closures? And each time it would be undefined? Only after the `if` statement runs do we assign `lastFn`. – 713sean Jan 26 '23 at 05:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/251401/discussion-between-713sean-and-certainperformance). – 713sean Jan 26 '23 at 05:33
1

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.

jsejcksn
  • 27,667
  • 4
  • 38
  • 62