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?
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.
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.
componentDidMount() is a hook that gets invoked right after a React component has been mounted aka after the first render() lifecycle.
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.
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...
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With