react: Bug: react-hooks/exhaustive-deps complains about useRef encapsulated in custom hook

So I’m using a custom usePrevious hook that returns the ‘previous value’ which is as far as I know pretty common practice. It leverages a ref inside the hook. When I declare a ref inside a functional component and use this in a useEffect, react-hooks/exhaustive-deps does not require me to add the ref to the dependencies list of a useEffect, but using usePrevious does.

React version: "react": "^17.0.1",

Steps To Reproduce

Link to code example: https://codepen.io/spassvogel/pen/JjbXKJL?editors=1111

const usePrevious = (value) => {
    const ref = React.useRef();
    React.useEffect(() => {
      ref.current = value;
    }, [value]);
    return ref.current;
};

const TestComponent = () => {
  const [state, setState] = React.useState(0);
  const previousState = usePrevious(state)
  
  React.useEffect(() => {
     console.log('current state', state)
     console.log('previous state', previousState)

  }, [state]); // ESLINT COMPLAINS 
  return null;
};

The current behavior

react-hooks/exhaustive-deps complains about the dependency list of useEffect is missing previousState.

The expected behavior

react-hooks/exhaustive-deps realizes that usePrevious is leveraging useRef and thus doesn’t need its return value in the dependency list.

A workaround would be to duplicate the code of usePrevious in every component, then react-hooks/exhaustive-deps does not complain. but yeah…

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 10
  • Comments: 15

Most upvoted comments

https://github.com/facebook/react/pull/20477

Looks like this PR would do it?

Would usePrevious need to return the ref instead of ref.current, since current will be changed on the render after every update? Seems wrong to ignore a value that actually is changing just as a matter of principle.

I understand the problem and sympathize with it, but the canonical solution that scales is that you simply include that as a dependency. Since it’s static and never changes, there is no issue including it. On the other hand, if you later do return something conditionally, like cond ? ref1 : ref2, then it will trigger a re-render instead of silently using a stale value. So this works precisely as designed.

Maybe a quick solution would be would be to add a configuration option to the linting rule where you can name what other hooks should be treated the same way as useRef. That way you could add usePrevious to the eslint config in your project and you’d effectively get the same behavior.