react icon indicating copy to clipboard operation
react copied to clipboard

Bug: react-hooks/exhaustive-deps - throws an unjustified warning for useEffect

Open sajera opened this issue 1 year ago • 5 comments

A fairly simple and straightforward code sample is causing a warning. I might be wrong, and I apologize if that's the case, but this seems like a bug on your end.

React Hook useEffect received a function whose dependencies are unknown. Pass an inline function instead

React version: ^18.2.0

Steps To Reproduce

  1. Run lint on the code below.
const foo = { bar: () => console.log('Do something once at component mount') }

export default memo(function Example () {
  useEffect(foo.bar, [])
  return <div />
})

As you might guess, there is a function that should be triggered once when the component mounts. It returns undefined...

The current behavior

Throws the warning: React Hook useEffect received a function whose dependencies are unknown. Pass an inline function instead

The expected behavior

Not to throw

sajera avatar Oct 12 '24 22:10 sajera

Suggested Solution for react-hooks/exhaustive-deps Warning

I believe the issue is related to how react-hooks/exhaustive-deps interprets dependencies when using external object method. The warning arises because useEffect receives a function (foo.bar) that isn't declared inline, causing EsLint to think that it might change between renders.

Proposed Solution: Use an inline function

One way to resolve this is by using an inline function within useEffect:

  useEffect(() => {
    console.log('Do something once at component mount');
  }, []);
  return <div />;
});

This approach eliminates the warning because the function is defined directly inside useEffect and does not have unknown dependencies.

Alternative Solution: Use useCallback to Memoize the Function If you prefer to use the method foo.bar, you can memoize it using

useCallback:


export default memo(function Example() {
  const stableBar = useCallback(foo.bar, []);
  useEffect(stableBar, []);
  return <div />;
});

This will ensure foo.bar is treated as a stable reference, avoiding unnecessary re-renders and warnings.

Let me know if this helps resolve the issue!

KhushiPandey8 avatar Oct 13 '24 09:10 KhushiPandey8

/assigned

Tanmay-008 avatar Oct 13 '24 19:10 Tanmay-008

You might have mismatching versions of React and the renderer (such as React DOM). You might be breaking the Rules of Hooks. You might have more than one copy of React in the same app. Please refer to React's documentation for tips on how to debug and fix this problem.

It seems that in your case, you passed a named function (foo.bar) directly to useEffect, which caused React to have difficulty understanding the dependencies of that function.

The solution to this issue is to use an inline function or an arrow function instead. This will provide React with a clearer understanding of the dependencies, allowing it to manage the effect correctly.

Tanmay-008 avatar Oct 13 '24 19:10 Tanmay-008

@Tanmayshi - You're right. I know several ways to avoid this warning, but I think the rules are flawed. They shouldn't guess; they should help prevent errors or unexpected behavior. In my case, the code is clear and error-free, so this warning seems like a bug.

P.S. Your code example looks massive. I just don't like using that coding style ;)

P.S. 2 useCallback(foo.bar, []) is logically unexpected here and requires at least some additional explanation. useEffect(foo.bar, []) clearly indicates that we expect only the component's mount and unmount events.

sajera avatar Oct 14 '24 07:10 sajera

BTW more samples of unexpected behavior based on your comments:

  1. useCallback may face the same problem as useEffect—why is the dependency check for it different compared to useEffect?
const foo = { bar: () => console.log('Do something once at component mount') }

const e1 = memo(function Example () {
  useEffect(foo.bar, []) // throw warning
  return <div />
})

const e2 = memo(function Example () {
  useCallback(foo.bar, []) // not to throw warning ?
  return <div />
})
  1. useEffect, from a rule perspective, behaves differently depending on the parent object of the function. Why is that?
const foo = () => console.log('Do something once at component mount')

const e1 = memo(function Example () {
  useEffect(foo, []) // not to throw warning ?
  return <div />
})

sajera avatar Oct 14 '24 07:10 sajera

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

github-actions[bot] avatar Jan 12 '25 08:01 github-actions[bot]

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

github-actions[bot] avatar Jan 19 '25 09:01 github-actions[bot]

@sajera The problem is that there is no way to know for sure that foo.bar doesn't change between renders. However, there is a very simple solution:

const foo = { bar: () => console.log('Do something once at component mount') }

const { bar } = foo

export default memo(function Example () {
  useEffect(bar, [])
  return <div />
})

Since bar is a constant, the linter plugin can be sure it won't change even if foo.bar does, and so can treat it as a stable reference.

@KhushiPandey8's solution with useCallback doesn't work for the exact reason you named: it faces the same problem as useEffect. Just like with useEffect, there is no way to tell that foo.bar is stable. The linter plugin does actually produce a warning for useCallback, too.

But what's weird is that it doesn't produce one when I replace const { bar } = foo in my solution with let { bar } = foo, making bar just as unstable as foo.bar. This is what feels like a bug to me, I will open a new issue for it.

aweebit avatar May 31 '25 17:05 aweebit