_isBatak
About

A real use-case for useEffect without dependency array

Ivica Batinić

A real use-case for useEffect without dependency array

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 useEffects 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]);
}

by Ivica Batinić with