Is the "useEffect has a missing dependency" warning sometimes wrong?

958 views Asked by At

I've been using hooks for a while and I never fully understand why React is forcing me to includes some dependencies that I don't want on my useEffect.

The way I understand the 'dependencies' of a useEffect hook

Add the values you want to 'listen' whenever they change and trigger your effect. This works perfectly with a simple effect like:

import React, {useEffect, useState} from "react";

interface Props {
    id: string
}

const SimpleComponent = (props: Props) => {
    const {id} = props;
    const [response, setResponse] = useState<object>();
    
    useEffect(() => {
        fetch(`https://myexample/${id}`)
            .then(response => setResponse(response))
            .catch(() => console.log("An error occurs!"))
    }, [id])
    
    return <div/>
};

However, there are some other cases where is not as straightforward as the example above. In this example we want to trigger the effect only when the id changes:

import React, {useEffect} from "react";

interface Props {
    id: string
    callback: Function
}

const SimpleComponent = (props: Props) => {
    const {id, callback} = props;
    
    useEffect(() => {
        callback(id)
    }, [id]);

    return <div/>
};

In this example, I get the warning "React Hook useEffect has a missing dependency" it recommends to include 'callback' on the dependencies array (option 1) or remove the dependencies array (option 2).

Let's explore the recommendations:

  • Option 1 (Include 'callback' on the dependencies array): Including 'callback' on the dependencies array will cause my effect to trigger whenever the 'id' or the 'callback' changes. The problem with this is that I don't want to trigger the effect when the 'callback' changes cause the callback will change in every render.

  • Option 2 (remove the dependencies array): Removing the dependency array will cause my effect to trigger whenever the component changes which is not the wanted behavior either.

Other options found:

I've found some other advice from the community but all of them seem to not accomplish the wanted behavior. (https://stackoverflow.com/a/60327893/8168782)

Let's quickly review these options:

Option 1: Use an empty dependencies array:

It will only trigger when the component mounts, not what we want.

Option 2: Declare the function inside the useEffect()

In this case, the 'callback' is a function passed through props, but either way most of the time you can't declare the function inside the effect because the function is used in other places.

Option 3: Memoize with useCallback()

If you wrap your function inside a useCallback you also need to include the dependencies into the useCallback dependencies array, this will cause the useCallback to trigger again every time the dependencies change so the useEffect also will be triggered.

Option 4: Disable eslint's warnings

Not considered because I'm trying to understand the problem not simply ignore it.

I'm really confused about this warning, I don't know if there are some situations where the warning is wrong and should be ignored (what seems to be wrong) or that I'm missing something.

1

There are 1 answers

0
hackape On BEST ANSWER

I personally always disable that eslint rule. Due to that eslint has no way to understand your logical intension, it can only exhaustively check all variables captured in the closure and warn you about missing ones from dep-list. But a lot of time it's overkilling, just like in your use case. That's the reasoning behind my choice.

If you have a clear understanding of how useEffect works, disabling this rule wouldn't cause much pain. I personally don't remember experiencing it.

A second solution is to leave the rule on, but work around it. I got one for you, the useFn custom hook:

function useFn(fn) {
  const ref = useRef(fn);
  ref.current = fn;

  function wrapper() {
    return ref.current.apply(this, arguments)
  }

  return useRef(wrapper).current
}

This hook returns a stable reference of wrapper function, which is just a proxy that call the actual fn, but doesn't change across re-rendering.

const SimpleComponent = (props: Props) => {
    const {id, callback: _callback} = props;

    const callback = useFn(_callback)
    
    useEffect(() => {
        callback(id)
    }, [id, callback]);

    return <div/>
};

Now you satisfy that eslint rule meanwhile you don't trigger unwanted useEffect re-run.


As an off-topic side note. I also use useFn hook to wrap functions that got passed to child components' props.

Passing arrow function around is heavily used pattern in React. Sometimes you have a component that's expensive to re-render, you React.memo(Component) wrap it, yet then you pass a <Component onClick={e => { ... }} /> inline function, which effectively invalidate the memoize effect. useFn comes to rescue:

<Component onClick={useFn(e => { ... })} />