I noticed that react’s exhaustive-deps line rule doesn’t always play nice with the setState function or when you abstract out customHooks.
For example, if I have a customHook like:
function useValidation(initialThings) {
const [needsValidation, setNeedsValidation] = useState(false);
const [thingsToValidate, setThingsToValidate] = useState<Things[]>(initialThings);
useEffect(() => {
debounceValidateThings(thingsToValidate);
}, [things, debounceValidateThings]);
return {
needsValidation,
setNeedsValidation,
thingsToValidate,
setThingsToValidate,
}
}
and I use it one of the setState functions outside of the hook:
const validationHook = useValidation(initialThings)
useEffect(() => {
// Add something to validate
validationHook.setThingsToValidate(newThings)
validationHook.setNeedsValidation(true)
}, [newThings]);
I noticed this returns a warning with exhaustive-deps. It suggest that I add the entire useValidation
to the depth which might causes excessive rerenders.
At the same time, it won't let me just add the setState calls: e.g. setThingsToValidate
or setNeedsValidation
It still recommends that:
Is there a way around this that doesn’t involve a lint warning? Or this there a paradigm here for abstracting out hooks that I’m missing??
This here:
return {
needsValidation,
setNeedsValidation,
thingsToValidate,
setThingsToValidate,
}
Creates a new object every time your hook runs. Then when you use that object as a hook dependency:
useEffect(() => {
// Add something to validate
validationHook.setThingsToValidate(newThings)
validationHook.setNeedsValidation(true)
}, [newThings, validationHook]);
The hook is invalidated every render. It's absolutely correct to list this object as a dependency, because it is a dependency.
So you have a few choices.
You could memoize the returned object:
return useMemo(() => ({
needsValidation,
setNeedsValidation,
thingsToValidate,
setThingsToValidate,
}, [
needsValidation,
thingsToValidate,
// eslint should know state setters are stable in this scope
// so they can be omitted
])
And now the identity of the returned object is preserved unless state changes. This makes it safe to use an effect dependency.
But I think the better approach would be deconstruct the returned object so you just used the property values. The state setters should be stable, and anything that depends on the state values needs to be updated anyway.
const {
needsValidation,
setNeedsValidation,
thingsToValidate,
setThingsToValidate,
} = useValidation(initialThings)
useEffect(() => {
setThingsToValidate(newThings)
setNeedsValidation(true)
}, [newThings, setThingsToValidate, setNeedsValidation]);
Or you could list properties of the returned object as hook dependencies:
const validationHook = useValidation(initialThings)
useEffect(() => {
// Add something to validate
validationHook.setThingsToValidate(newThings)
validationHook.setNeedsValidation(true)
}, [
newThings,
validationHook.setThingsToValidate,
validationHook.setNeedsValidation,
]);