I have recently started to use the new React Hooks API and I find it awesome!
However, I ran into a small confusion in the dependencies area.
Basically, my use case is pretty simple, and can be illustrated by the following pseudo code:
import React, { useState, useCallback, useEffect } from 'react'
function Component() {
const [state, setState] = useState()
const doStuff = useCallback(() => {
// Do something
setState(result)
}, [setState])
useEffect(() => {
// Do stuff ONLY at mount time
doStuff()
}, [])
return <ExpensivePureComponent doStuff={doStuff} />
}
Now, the above code works fine.
But after I installed eslint-plugin-react-hooks
, there is a warning. I must declare all dependencies that is use in my effects, which is, here, doStuff
.
Fine, let's fix that code:
useEffect(() => {
// Do stuff ONLY at mount time
doStuff()
}, [doStuff])
Cool, no more warning!
Let's see what the docs say about useCallback
:
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
And then, about useMemo
:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render
So, basically, my doStuff
callback, hence my useEffect
, isn't guaranteed anymore to only run at mount time? Isn't that a problem?
I understand the principles behind the eslint plugin, but it looks to me that there is a dangerous confusion between useCalback
/ useMemo
dependencies arrays, and useEffect
one's, or am I missing something?
It might be, because even the docs say that my final code is fine:
If for some reason you can’t move a function inside an effect, there are a few more options:
● ...
● As a last resort, you can add a function to effect dependencies but wrap its definition into the useCallback Hook. This ensures it doesn’t change on every render unless its own dependencies also change
Well, what do you think? Code is safe? Docs say it is, but also say it is not, because callback is not guaranteed to not change... this is a bit confusing.
Is there a bad practice in the above pseudo code? When that pattern cannot be avoided, what to do? // eslint-disable-next-line
?
The warning "React Hook useEffect has a missing dependency" occurs when the useEffect hook makes use of a variable or function that we haven't included in its dependencies array. To solve the error, disable the rule for a line or move the variable inside the useEffect hook.
Missing dependencies are those dependencies that are not available in the repository, so you cannot add them to your deployment set. You can set Deployer to ignore missing dependencies when you create the project (see Creating a Project) or when you check unresolved dependencies.
The useEffect hook allows you to perform side effects in a functional component. There is a dependency array to control when the effect should run. It runs when the component is mounted and when it is re-rendered while a dependency of the useEffect has changed.
While the docs say that
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
it doesn't mean that useCallback is implement using useMemo
and it certainly is not. So while useMemo may choose to calculate again useCallback won't update the function unless the something in dependency array changes.
Also since the setter returned by useState
doesn't change, you don't need to pass it on to the useCallback
const doStuff = useCallback(() => {
// Do something
setState(result)
}, [])
Since doStuff won't change, useEffect
won't be called again apart from initial mount.
One thing however that you should keep in mind while using useEffect
and useCallback
is that if you dependency array in useCallback changes the callback will be recreated and hence useEffect will re-rerun. One way to prevent such scenarios is to make use of useReducer
hook instead of useState
and rely on dispatch
to update state since it won't ever change during the course of your App interactions in a session.
import React, { useReducer, useEffect } from 'react'
const initialState = [];
const reducer = (state, action) => {
switch(action.type) {
case 'UPDATE_STATE' : {
return action.payload
}
default: return state;
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
// Do stuff ONLY at mount time
dispatch({type: 'UPDATE_RESULT', payload: ['xyz']})
}, [])
return <ExpensivePureComponent dispatch={dispatch} />
}
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