React - Is useState 's setter function able to change?

The short answer is, no, the setter of useState() is not able change, and the React docs explicitly guarantee this and even provide examples proving that the setter can be omitted.

I would suggest that you do not add anything to the dependencies list of your useCallback() unless you know its value can change. Just like you wouldn't add any functions imported from modules or module-level functions, constant expressions defined outside the component, etc. adding those things is just superfluous and makes it harder to read your handlers.

All that being said, this is all very specific to the function that is returned by useState() and there is no reason to extend that line of reasoning to every possible custom hook that may return a function. The reason is that the React docs explicitly guarantee the stable behavior of useState() and its setters, but it does not say that the same must be true for any custom hook.

React hooks are still kind of a new and experimental concept and we need to make sure we encourage each other to make them as readable as possible, and more importantly, to understand what they actually do and why. If we don't it will be seen as evidence that hooks are a "bad idea," which will prohibit adoption and wider understanding of them. That would be bad; in my experience they tend to produce much cleaner alternatives to the class-based components that React is usually associated with, not to mention the fact that they can allow organizational techniques that simply aren't possible with classes.


The setter function won't change during component life.

From Hooks FAQ:

(The identity of the setCount function is guaranteed to be stable so it’s safe to omit.)

The setter function (setState) returned from useState changes on component re-mount, but either way, the callback will get a new instance.

It's a good practice to add state setter in the dependency array ([setState]) when using custom-hooks. For example, useDispatch of react-redux gets new instance on every render, you may get undesired behavior without:

// Custom hook
import { useDispatch } from "react-redux";

export const CounterComponent = ({ value }) => {
  // Always new instance
  const dispatch = useDispatch();

  // Should be in a callback
  const incrementCounter = useCallback(
    () => dispatch({ type: "increment-counter" }),
    [dispatch]
  );

  return (
    <div>
      <span>{value}</span>

      // May render unnecessarily due to the changed reference
      <MyIncrementButton onIncrement={dispatch} />

      // In callback, all fine
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  );
};