facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.52k stars 46.77k forks source link

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

Open sajera opened 1 week ago

sajera commented 1 week ago

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

xing24xing commented 1 week ago

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!

Tanmayshi commented 1 week ago

/assigned

Tanmayshi commented 1 week ago

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.

sajera commented 1 week ago

@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 commented 1 week ago

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

})

const e2 = memo(function Example () { useCallback(foo.bar, []) // not to throw warning ? return

})

2. **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

})