score:15

Accepted answer

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.


Related Query

More Query from same tag