A real use-case for useEffect without dependency array
Ivica Batinić
I finally Found a real use-case for useEffect without deps array.
With this approach you don’t have to wrap callback function in useCallback
.
And you don’t have to violate rules of hooks by excluding callback dependency in the second useEffect
.
To overcome this limitation developers usually do this:
export default function useInterval(callback, delay) {
useEffect(() => {
if (!delay) {
return () => {};
}
const interval = setInterval(() => {
callback();
}, delay);
return () => clearInterval(interval);
// Ignore the rule of hook by excluding callback dependency
}, [delay]);
}
But this is problematic because now callback is inside the closure of useEffect and will never be updated during the lifecycle.
So something like this will never work:
useInterval(() => {
if (isAlmostDone) {
console.warn('Almost done!');
} else {
console.warn('So close!');
}
}, 1000);
If isAlmostDone
state is flipped it will never be detected by the useInterval
.
Assigning ref
synchronously might be a good solution for now, but it will cause some hard to detect bugs in Concurrent mode.
export default function useInterval(callback, delay) {
const callbacRef = useRef();
// In concurrent mode React may decide to stop executing this block of code and continue it later on (high/low priority).
// Because of this you might end up with a wrong reference.
callbacRef.current = callback;
useEffect(() => {
if (!delay) {
return () => {};
}
const interval = setInterval(() => {
callbacRef.current && callbacRef.current();
}, delay);
return () => clearInterval(interval);
}, [delay]);
}
So the safest solution is to wrap ref
assignment in the useEffect.
Callback ref will be assigned on every render, but only at the end of mount/update cycle, and React will assure that useEffect
s are executed in the correct order.
function useInterval(callback, delay) {
const callbacRef = useRef();
useEffect(() => {
callbacRef.current = callback;
});
useEffect(() => {
if (!delay) {
return () => {};
}
const interval = setInterval(() => {
callbacRef.current && callbacRef.current();
}, delay);
return () => clearInterval(interval);
}, [delay]);
}