3

I realize there have been many questions about React's useEffect hook's dependency array, and the eslint warning that may arise from missing dependencies. Some other good discussions about this:

Although the causes & solutions for these warnings are very clear, one thing I'm struggling to grasp is the why. First, to quickly summarize, my understanding is that useEffect as three general purposes:

  • If its dependency array is omitted, it acts like both componentDidMount and componentDidUpdate: it runs the first time the component is rendered, and all subsequent renders.
  • If its dependency array is empty, it acts like componentDidMount: it can be used to run first-time initialization code (e.g. fetching initial data from the server, etc).
  • Otherwise, you can include in its dependency array any variables that you want it to 'watch'; it will act like componentDidMount, and then like componentDidUpdate when those variables change.
  • (For completeness: its return value can also be used to implement componentWillUnmount behavior, but that's beyond the scope of this issue).

I understand that eslint will complain if useEffect references any functions (or other variables) which are declared outside of useEffect. Per the above links, there are various ways to resolve these warnings: Move the function definition inside useEffect (if nothing outside useEffect needs to call it); add the function to the dependency array & memoize it with useCallback; disable the warning with // eslint-disable-next-line or // eslint-disable-line react-hooks/exhaustive-deps; etc. These all make sense. What I'm struggling to understand is WHY it even complains about this.

So, my specific questions:

  1. Practically speaking, I can't understand why you would ever want a reference to a local function in the dependency array. For example, if you have useEffect(() => { checkCurrentUser() }, []), and checkCurrentUser needs to be defined outside useEffect because it's called elsewhere, checkCurrentUser will be re-defined on every component render (which is why if we put it in the array, we should memoize it w/ useCallback). But to me, it makes no logical sense why one would ever want this in the dependency array at all. So why is this even a warning? What is this warning actually helping to prevent? Throughout all the times I've seen the warning, in every case, the application has behaved exactly as I expected/intended.

  2. If one of useEffect's primary purposes is to 'run once' like componentDidMount, and the way to achieve that behavior is to explicitly put an empty array, why does it hassle you if you specify an empty array? I can understand why it might make sense to warn if you have a dependency array that contains some references but not others - as in 'oops, you forgot some' - but the empty array is a specific, well-defined usage. To me, it doesn't seem to make sense that it would tell you 'an empty array is an issue,' as that's the only way to achieve componentDidMount-type initialization. It feels like the same thing as just warning you "don't use componentDidMount()." Why does it complain about an empty array, if that's supposedly one of the 3 main uses of useEffect?

Again, I do understand the circumstances in which the warning appears, and the ways to resolve it. I just struggle to understand the benefit of even having it, (1) in the case of functions (which we definitively know will change on every single render), and (2) in the case of an empty array (which are a specific usage).

Nimantha
  • 6,405
  • 6
  • 28
  • 69
J23
  • 3,061
  • 6
  • 41
  • 52

1 Answers1

2

(1) in the case of functions (which we definitively know will change on every single render)

The reason for requiring to put functions in dependencies seems to be the same as the reason why they require you to put any other values in dependencies: what can happen is that a function you are using might be itself referencing some values from component scope, which can become stale.

(2) in the case of an empty array (which are a specific usage).

In this case I will reference answer cited by Dan Abramov from this comment:

I think the biggest gotcha with class life cycle methods like componentDidMount is that we tend to think of it as an isolated method, but in fact it's part of a flow. If you reference something in componentDidMount you will most probably need to handle it in componentDidUpdate as well, or your component may get buggy. This is what the rule is trying to fix, you need to handle values over time....

You may find more info on why they did it that way in that thread.

Giorgi Moniava
  • 27,046
  • 9
  • 53
  • 90
  • 1
    This is correct, and as for the OP's question about wanting an effect to only run one time, but having to reference the function in the dependencies array: If your function doesn't change references, it really will only run one time, and that's logically consistent. As noted by the author, useCallback is the correct solution for the scenario they were referencing. – Cory Harper May 06 '21 at 17:54
  • Re: the first answer, ohhh, that makes sense!! Do you think it would be fair to say "As a rule, whenever we have functions that are called both within useEffect() and elsewhere, wrap them in useCallback & put them in useEffect's deps array, and it behaves the same as if the array were empty?" – J23 May 06 '21 at 18:10
  • In the second case, I guess that specific usage is possible - however, it feels like an extremely uncommon/corner case, which the linter could just look for explicitly (i.e. it could look for setInterval). If you set aside setInterval specifically, does it still make sense to you to warn against an empty array? – J23 May 06 '21 at 18:12
  • @CoryHarper Sorry not sure what you mean. – Giorgi Moniava May 06 '21 at 18:27
  • 1
    @Metal450 what you said about `useCallback` with `useEffect` is true, but only if the dependencies of `useCallback` don't change. – Cory Harper May 06 '21 at 18:44
  • @gmoniava, other than the part where I was saying your answer is correct, the rest of my comment was dedicated to things that were said in the original question, that I didn't want to tag another answer to the question on for. – Cory Harper May 06 '21 at 18:45
  • @CoryHarper: Ah, makes sense - thanks again! – J23 May 06 '21 at 18:51
  • @Metal450 I updated my answer. Not sure what you meant with first comment above. – Giorgi Moniava May 06 '21 at 18:52
  • @gmoniava: Clear, understood now - thanks! Accepted & upvoted :) – J23 May 06 '21 at 18:54
  • @CoryHarper I didn't entirely understand what you meant in your first comment (even though it was about OPs question) that's why I asked – Giorgi Moniava May 06 '21 at 19:24
  • @gmoniava not to worry, to clear any confusion you may have: In the original question the poster brought up a scenario "For example, if you have `useEffect(() => { checkCurrentUser() }, [])`" and asked why it doesn't make sense to be able to use an empty array in that scenario. My comment was to address that for him. – Cory Harper May 06 '21 at 19:27
  • @CoryHarper I got confused by this part of yours: "If your function doesn't change references, it really will only run one time, and that's logically consistent. As noted by the author, useCallback is the correct solution for the scenario they were referencing". Can you elaborate what you meant here? – Giorgi Moniava May 06 '21 at 19:32
  • If you don't use `useCallback`, and your function is defined inside of the component, it is being redefined on each render. So the `useEffect` is not going to assume that a completely new function has all the same references, returns values, etc. as the old function it was working with, ergo the need to put it in the dependency array. When you don't memoize the function in some way, this leads to the effect running on every render. By memoizing the function, you allow useEffect to be informed about when that function has actually changed. – Cory Harper May 06 '21 at 19:47
  • In the above scenario, if `checkCurrentUser` is memoized, and really does never change after mount, the effect will only run one time, even having put it in the dependency array. – Cory Harper May 06 '21 at 19:47
  • From these docs: https://reactjs.org/docs/hooks-reference.html#usecallback: "This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate)." – Cory Harper May 06 '21 at 19:50
  • @CoryHarper Above you basically explained how to use `useCallback` to prevent re renders when there is function in `useEffect` deps right? I know that. But I had feeling OPs question was not about that, it was about why put a function in dependency – Giorgi Moniava May 06 '21 at 19:56
  • @CoryHarper But ok at least I better see now what point you were trying to make. Ok, we can wrap up I suppose, because I think long discussions in comments aren't encouraged on this site. – Giorgi Moniava May 06 '21 at 20:10