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.
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:This hook returns a stable reference of
wrapper
function, which is just a proxy that call the actualfn
, but doesn't change across re-rendering.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: