I’m an experienced programmer (10+ years) but new to React (< 2 years). I picked up React in a very "operational" way (i.e. being happy when things work without very deep understanding of its inner workings). Now I’m trying to correct that.
One thing that surprises me is how easily it lets you do the wrong thing. ESLint might warn you, but the fix might be completely misleading. Here’s one situation in which I’m trying to figure out what the best practice is.
function MyComponent() {
const [functionA, functionB] = useMyHooks();
const shouldOnlyCallOnce = useCallback(() => {
functionA();
functionB();
}
[]);
useEffect(() => {
shouldOnlyCallOnce();
}, []);
...
}
Now ESLint will complain that functionA and functionB are not in the dependency array and suggest that I either (a) add them to the dependency array, or (b) remove the dependency array. I think both of these suggestions are wrong, and I’m not sure what the best practice is here.
Here’s what I think:
(a) If I want to code defensively, I should not make any assumptions about when functionA and functionB are re-defined. They might be redefined on every rerender. So to go this route, ESLint should also suggest that I memoize them first.
(b) Removing the dependency array would have the opposite effect from what I want, which is to run shouldOnlyCallOnce a single time.
So what’s the right pattern here? Do (a) with additional memoization? (But I also heard that excessive memoization can degrade performance).
2
Answers
Both ESLint suggestions aren’t ideal. The best approach would be to memoize
functionA
andfunctionB
(if they change or rerender) and then add them to the dependency array. This way, your callback stays stable and runs as intended without over-memoizing.According to the ESLint React hooks rules then your option (A) is correct, both
functionA
andfunctionB
are external dependencies (to the hook) and should be included in theuseCallback
dependency array such that they are re-enclosed in callback scope when their references change. Option (B) and omitting the dependency array entirely is just about wrong 100% of the time and would be exactly the same as not usinguseCallback
at all. The idea here is to provide stable callback references as much as possible.This creates
shouldOnlyCallOnce
as a stable memoized callback function to be used/called further down the line, e.g. passingshouldOnlyCallOnce
as a stable reference as props to children components.If you only want
functionA
andfunctionB
to be called once, immediately then this sounds more like you want to just call them as intentional side-effects right then and there inMyComponent
. If this is the case then you should instead use theuseEffect
hook.Examples:
Empty dependency array means invoke the
useEffect
hook callback exactly once when the component mounts.Include
functionA
andfunctionB
as dependencies when you wish for both of them to be called when either changes references:Ideally your
useMyHooks
should also already be usinguseCallback
to memoize and provide stablefunctionA
andfunctionB
references to its consumers, otherwise you force all consumers to do this each time they want, or care, for them to also be stable references.