7

I'm well aware of what the Hook has missing dependency is, what it means and why it's important to watch all dependencies, but this one is just weird.

export function Compo() {
  const [value, setValue] = useState<number>();

  useEffect(() => {
    setValue(Date.now());
  }, []);

  return (
    <>{value}</>
  );
}

works fine, but:

function useValue() {
  return useState<number>();
}

export function Compo() {
  const [value, setValue] = useValue();

  useEffect(() => {
    setValue(Date.now());
  }, []);

  return (
    <>{value}</>
  );
}

show the well known React Hook useEffect has a missing dependency: 'setValue'. Either include it or remove the dependency array react-hooks/exhaustive-deps.

2 Answers 2

9

What you've noticed in your example is a quirk of the rule react-hooks/exhaustive-deps. It gives special privilege to hooks it is aware of, and knows to be "stable" under certain circumstances.

Quoting the implementation:

// Next we'll define a few helpers that helps us
// tell if some values don't have to be declared as deps.

// Some are known to be stable based on Hook calls.
// const [state, setState] = useState() / React.useState()
//               ^^^ true for this reference
// const [state, dispatch] = useReducer() / React.useReducer()
//               ^^^ true for this reference
// const ref = useRef()
//       ^^^ true for this reference
// False for everything else.

source: https://github.com/facebook/react/blob/v17.0.1/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js#L152

Specifically, this part of the rule seems to be what is exempting the useState hook's setter under these circumstances:

if (name === 'useState') {
  const references = resolved.references;
  for (let i = 0; i < references.length; i++) {
    setStateCallSites.set(
      references[i].identifier,
      id.elements[0],
    );
  }
}
// Setter is stable.
return true;

The unfortunate result of the hook being helpfuln/clever is that it can lead to confusion where its inference doesn't work, like the scenario you just described.

Sign up to request clarification or add additional context in comments.

2 Comments

I think I've never seen anyone go that deep into a response, thank you for the effort ! So basically I just have to find a way around it. The rule isn't powerful enough to find dynamic references through custom hooks I assume. Any idea how I would go around it ?
It's not detrimental to include the setter in the dependencies array, it's just with a deep enough inference you wouldn't need to. Same with useState, you could still add it without changing the behavior, it's just the eslint plugin is "smart" enough to know you don't need it.
0

I have to correct my previous spontaneous answer which was: I do not agree that "it's not detrimental to include the setter in the dependencies array". Adding the setter to the dependencies array will cause the effect function to be called after every render of the component because the setter returned by the custom hook will technically always be a new function though calling it will have the same effect. So, in my opinion, the correct answer would be that you have to manually deactivate the eslint rule if and only if you are sure that it does not matter if the very first returned setter is used or one that is returned later when there is a rerender and so a re-call of the custom hook function.

If you are 100% certain that a dependency from a custom setter is not necessary, you can use eslint-disable-line. But, it is dangerous if you really forget some other dependencies in your effect code.

For example

const [myValue, mySetter] = useMyCustomHook();
useEffect(() => {
    mySetter('bla');
},
// eslint-disable-next-line
[])

Meanwhile, I have realized that in the specific case that the original setter of the state is returned in the custom hook, is really not changing because it obviously is implemented as a kind of "static callback" which does not change on re-rendering. I would like to mention here how this can be achieved also for a custom setter function using the hook "useCallback":

export function useDoubleValue() {
    const [doubleValue, setDoubleValue] = useState<number>(0);
    const setSingleValue = useCallback((singleValue: number) => {
        setDoubleValue(singleValue * 2);
    }, [/* optional dependency from setDoubleValue - does not matter */]);

    return [doubleValue, setSingleValue];
}

Now, as setSingleValue is implemented using the hook useCallback(), the function is only created once, here on mounting. So, if a page using our hook "useDoubleValue" will not re-render on every re-render ending up in a confusing render-loop. And, which is nice, we can add the dependency of the setter "setSingleValue" to the dependency list of an effect where it is called because (to avoid the warning without needing the ugly and dangerous trick with esling-disable-next-line or the like), it will not cause the effect to re-run because as a stable callback there will be no change which makes the effect to re-execute.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.