Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement componentDidMount with hooks in React to be in line with the EsLint rule "react-hooks/exhaustive-deps": "warn"?

According to the official React documentation, componentDidMount is translated in hooks as:

useEffect(() => {
 //code here 
},[])

So assuming I want to do an api call within this hook:

useEffect(() => {
 getActiveUser(); 
},[])

After adding the eslint rule "react-hooks/exhaustive-deps", this is a lint error. In order to silence it I can just drop the getActiveUser function inside the array and everything works just fine.

But does that go against the documentation? I was under the impression that the array checks for prop changes. I would like also to point out that the API call is being made without a prop/id, so I could understand the fact of having to do something like that:

useEffect(() => {
 getActiveUser(someId); 
},[getActiveUser, someId])

So what's going on here? Adding the Eslint rule mean that the array inside the effect can't be empty again?

like image 332
Kleo Avatar asked Jul 19 '19 10:07

Kleo


People also ask

How do you resolve React hooks exhaustive DEPS?

The "react-hooks/exhaustive-deps" rule warns us when we have a missing dependency in an effect hook. To get rid of the warning, move the function or variable declaration inside of the useEffect hook, memoize arrays and objects that change on every render or disable the rule.

What is the equivalent of componentDidMount in React hooks?

The equivalent of componentDidMount in hooks is the useEffect function. Functions passed to useEffect are executed on every component rendering—unless you pass a second argument to it.

Is componentDidMount a hook?

componentDidMount() is a hook that gets invoked right after a React component has been mounted aka after the first render() lifecycle.

Can we use useEffect in componentDidMount?

If you're familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount , componentDidUpdate , and componentWillUnmount combined. There are two common kinds of side effects in React components: those that don't require cleanup, and those that do.


1 Answers

It matters where getActiveUser is declared. The question doesn't specify, but I assume your component looks something like this:

const MyComponent = (props) => {
    const getActiveUser() => {
       //...
    }
    useEffect(() => {
        getActiveUser();

    }, []) // Lint error.
    return <></>;
}

If instead your component looked like this, you wouldn't get a linter error:

const getActiveUser() => {
    //...
}
const MyComponent = (props) => {
    useEffect(() => {
        getActiveUser(); 

    }, []) // No error
    return <></>;
}

So why is the first a linter error and the second not? The point of the linter rule is to avoid issue due to stale props or state. While getActiveUser is not itself a prop or state, when its defined inside the component, it may depend on props or state, which may be stale.

Consider this code:

const MyComponent  = ({userId}) => {
    const [userData, setUserData] = useState(null);
    const getActiveUser() => {
        setUserData(getData(userId)); // More realistically this would be async
    }
    useEffect(() => {
        getActiveUser();
    }, []);

    //...
}

Even though that useEffect depends on the userId prop, it only runs once, and so the userId and the userData will be out of sync if the userId changes. Maybe this is your intent, but for the purposes of the linter rule it looks like a bug.

In the case where getActiveUser is defined outside the component, it can't possibly (or at least not reasonably) depend on the state or props of the component, so there's no issue for the linter rule.


So how to fix this? Well, if getActiveUser doesn't need to be defined inside the component, just move it out of the component.

Alternatively, if you're sure you only want this behavior to run when the component mounts, and that won't cause issue due to props changing (it's best to assume all props can change), then you can just disable the linter rule.

But assuming neither of those is the case...

A non-solution (too many effects)

As you've noted, adding getActiveUser to the linter array makes the issue go away:

const MyComponent = ({userId}) => {
    const getActiveUser() => {
       //...
    }
    useEffect(() => {
        getActiveUser();

    }, [getActiveUser]) // No error... but probably not right.
    return <></>;
}

But getActiveUser is a different function instance every render, so as far as useEffect is concerned, the deps array changes every render, which will cause an API call after every render, which is almost certainly not what you want.

A fragile solution

Since the root issue in my example is that the userId prop might change, you could also fix this issue by adding userId to the useEffect dependencies:

const MyComponent = ({userId}) => {
    const getActiveUser() => {
       // Uses userId
    }
    useEffect(() => {
        getActiveUser();

    // Linter is still unhappy, so:
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [userId])
    return <></>;
}

This behaves correctly - no extra API calls or stale data - but the linter is still unhappy: it isn't clever enough to know that we've fixed the dependency on getActiveUser by depending on all the things that getActiveUser depends on.

And this is fragile: if you add a prop or state in the future that getActiveUser depends on, and forget to add it here, you're going to have stale data issues.

A better solution

So the recommended solution is:

const MyComponent = ({userId}) => {
    const getActiveUsers = useCallback(() => {
        // uses userId
    }, [userId]);

    useEffect(() => {
        getActiveUser(); 

    }, [getActiveUsers]) // No error
    return <></>;
}

By wrapping getActiveUsers in useCallback, the function instance is only replaced when needed: when userId changes. This means that the useEffect also only runs when needed: when getActiveUsers changes (which is whenever userId changes).

The linter is happy with this solution and if you introduce new dependencies to getActiveUser, you'll only need to change its useCallback deps, not the useEffect.


Dan Abramov's blogpost A Complete Guide to useEffect goes into this in more detail.

like image 105
Retsam Avatar answered Oct 13 '22 07:10

Retsam