reactjstypescripttimer

Seeking a Restartable Timeout Hook in React


I'm building an app that displays images. Over top of each image, I wish to display an informative tooltip, but upon the mouse moving over top of the image, I want the tooltip to only display about 0.5 seconds after.

To this end, I have implemented this React Hook:

import * as React from 'react';

/**
 * This is a React Hook implementation of the window.setTimeout function.
 * Source: https://www.joshwcomeau.com/snippets/react-hooks/use-timeout
 * 
 * @param {callback} onTimerDone 
 * @param {number} delay - milliseconds
 */
export const useTimeout = (onTimerDone: () => void, delay: number) => {
  const timeoutRef = React.useRef(0);
  const savedCallback = React.useRef(onTimerDone);

  React.useEffect(() => {
    savedCallback.current = onTimerDone;
  }, [onTimerDone]);
  
  React.useEffect(() => {
    const tick = () => savedCallback.current();
    timeoutRef.current = window.setTimeout(tick, delay);

    return () => window.clearTimeout(timeoutRef.current); // Cleanup function
  }, [delay]);

  return timeoutRef;
};

I tried several approaches but can only get this timer hook to fire once. I'm looking for a timer that acts like a restartable timer. In other words, the timer will start counting down again & again whenever I instruct it to.

I can't figure out how to implement a Timeout hook that works in the way I need it to. Can anyone provide any suggestions?


Solution

  • If you need tooltip to show up after timeout you need to use event handler, not useEffect.

    // reusable hook to schedule timeouts
    function useTimeoutTask() {
      const timer = useRef<ReturnType<typeof setTimeout>>();
    
      const cancel = useCallback(() => {
        if (timer.current) {
          clearTimeout(timer.current);
          timer.current = undefined;
        }
      }, []);
    
      const schedule = useCallback((handler: () => void, timeout: number) => {
        // we only allow one active timeout at a time
        cancel();
    
        timer.current = setTimeout(handler, timeout);
      }, []);
    
      // Clear timeout on unmount
      useEffect(() => () => cancel(), []);
    
      return {
        schedule,
        cancel,
      };
    }
    
    // and example of application
    function useTooltip() {
      const [showTooltip, setShowTooltip] = useState(false);
      const { cancel, schedule } = useTimeoutTask();
    
      const onMouseEnter = () => {
        // setup task inside event handler
        schedule(() => {
          setShowTooltip(true);
        }, 500);
      };
    
      const onMouseLeave = () => {
        // cancel tooltip in case mouse leaves before tooltip was shown
        cancel();
        setShowTooltip(false);
      };
    }