React hooks useInterval reset after button click

Viewed 1376

I have hook useInterval which download data every 10 seconds automaticaly, however I have also button which can manually download data in every moment. I'm struggling to restart interval timer when I click button. So basically if interval counts to 5, but I click button meantime, interval should restart and starts counting to 10 again before downloading data

const useInterval = (callback, delay) => {
    const savedCallback = useRef(callback);

    useEffect(() => {
        savedCallback.current = callback;
    }, [callback]);

    useEffect(() => {
        const tick = () => {
            savedCallback.current();
        }

        if (delay !== null) {
            const id = setInterval(tick, delay);
            return () => clearInterval(id);
        }
    }, [delay]);
};

export default useInterval;

APP PART:

useInterval(() => {
        getMessage();
      }, 10000)

const getMessage = async () => {
    setProcessing(true)
    try {
      const res = await fetch('url')
      const response = await res.json();
      setRecievedData(response)
    }
    catch (e) {
      console.log(e)
    }
    finally {
      setProcessing(false)
    }
  }

const getMessageManually = () => {
    getMessage()
    RESTART INTERVAL
  }
3 Answers

You can add a reset function in the hook and return that function. The reset function should clear the existing interval and start a new one.

Here is the code for the hook which can be reset and stopped.

const useInterval = (callback, delay) => {
  const savedCallback = useRef(callback);
  const intervalRef = useRef(null);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay !== null) {
      const id = setInterval(savedCallback.current, delay);
      intervalRef.current = id;
      return () => clearInterval(id);
    }
  }, [delay]);

  useEffect(()=>{
    // clear interval on when component gets removed to avoid memory leaks
    return () => clearInterval(intervalRef.current);
  },[])

  const reset = useCallback(() => {
      if(intervalRef.current!==null){
        clearInterval(intervalRef.current);
        intervalRef.current = setInterval(savedCallback.current,delay)
      }
   });

   const stop = useCallback(() => {
      if(intervalRef.current!==null){
        clearInterval(intervalRef.current);
      }     
   })

  return {
    reset,
    stop
  };
};
    
// usage
const {reset,stop} = useInterval(()=>{},10000);
reset();
stop();

You should add a reset function as returning a value from the hook.

I also fixed few issues and added an unmount handler:

// Usage
const resetInterval = useInterval(() => ..., DELAY);
resetInterval(); 
// Implementation
const useInterval = (callback, delay) => {
  const savedCallbackRef = useRef(callback);

  const intervalIdRef = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // handle tick
  useEffect(() => {
    const tick = () => {
      savedCallback.current();
    };

    if (delay !== null) {
      intervalIdRef.current = setInterval(tick, delay);
    }

    const id = intervalIdRef.current;
    return () => {
      clearInterval(id);
    };
  }, [delay]);

  // handle unmount
  useEffect(() => {
    const id = intervalIdRef.current;
    return () => {
      clearInterval(id);
    };
  }, []);

  const resetInterval = useCallback(() => {
    clearInterval(intervalIdRef.current);
    intervalIdRef.current = setInterval(savedCallback.current, delay)
  }, [delay]);

  return resetInterval;
};

Another solution is to remove the ref on the callback making the hook restart the count on every change to the callback

so updating the above solution

// Implementation const useInterval = (callback, delay) => {

const intervalIdRef = useRef();

// handle tick useEffect(() => { const tick = () => { callback(); };

if (delay !== null) {
  intervalIdRef.current = setInterval(tick, delay);
}

const id = intervalIdRef.current;
return () => {
  clearInterval(id);
};

}, [delay]);

// handle unmount useEffect(() => { const id = intervalIdRef.current; return () => { clearInterval(id); }; }, []); };

And then you can use it like this

const [counter, setCounter] = useState[0]
const onTimerFinish = useCallback(() => {
  setCounter(counter + 1)
  // setCounter will reset the interval
}, [counter])

useResetInterval(() => {
  onTimerFinish()
}, 5000)
Related