I put the simplified version of my case above. I'm trying to compare the new network result with the previous version of it. I store fetched data in the state as 'notifications'. I don't want to setNotifications
if there is no change.
In useEffect dependency array, eslint yells and wants me to put the notifications
array into the dep array. But I'm changing notifications
in useEffect. If I'm not wrong, it will be an endless loop. Because notifications are an object array and they change in each render (even if there is no real change in data)
Could you explain which mental model should I follow when using useEffect in this scenario?
const NotificationBox = () => {
const [notifications, setNotifications] = useState([])
useEffect(() => {
client.get('notification?count=10').then((newResult) => {
if (!_.isEqual(notifications, newResult)) {
//notifications is stale
setNotifications(newResult)
}
})
}, [iHaveSomethingDifferentThatTriggersThisEffect])
//Also, ESLint: React Hook useEffect has a missing dependency: 'notifications'. Either include it or
remove the dependency array.(react-hooks/exhaustive-deps)
}
export default NotificationBox
My guess is that your code will work fine... almost all the time.
If notifications
is updated and the fetch is triggered at the same time your hook runs the risk of running with a stale value.
I think you can do better. You could for example make a custom hook to encapsulate the behaviour.
const notificationsCache = {
current: []
}
export function useNotifications() {
const [notifications, setNotifications] = useState(notificationsCache.current)
const update = useCallback(() => {
client.get('notification?count=10').then((newResult) => {
if (!_.isEqual(notificationsCache.current, newResult)) {
notificationsCache.current = newResult
setNotifications(notificationsCache.current)
}
})
}, [])
return [notifications, update]
}
This uses and exernal "ref" to cache the result, so all the component using the hook will share a cache. If that's not desired, you can just move the cache into the component with a useRef
instead and every component will have an independent cache.
export function useNotifications() {
const notificationsCache = useRef([])
const [notifications, setNotifications] = useState(notificationsCache.current)
const update = useCallback(() => {
client.get('notification?count=10').then((newResult) => {
if (!_.isEqual(notificationsCache.current, newResult)) {
notificationsCache.current = newResult
setNotifications(notificationsCache.current)
}
})
}, [])
return [notifications, update]
}
Then we just can
const [notifications, update] = useNotifications()
and use update
when we want to trigger a re-fetch.
There is room to improve this more. What if you have multiple components that use the hook and are mounted at the same time. If you updates the value, the other hooks state should ideally also get updated.
A perfect time to use the new useId
hook.
const notificationsCache = {
current: []
}
const listeners = {}
export function useNotifications() {
const id = useId()
const [notifications, setNotifications] = useState(notificationsCache.current)
useEffect(() => {
listeners[id] = setNotifications
return () => delete listeners[id]
}, [])
const setState = useCallback((newNotifications) => {
notificationsCache.current = newNotifications
for (const id in listeners) {
listeners[id](notificationsCache.current)
}
}, [])
const update = useCallback(() => {
client.get('notification?count=10').then((newResult) => {
if (!_.isEqual(notificationsCache.current, newResult)) {
setState(newResult)
}
})
}, [])
return [notifications, setState, update]
}