reactjsreact-hookstimersetintervalcountdown

How to use useState, Dispatch input?


Consider this code:

const [seconds, setSeconds] = useState<number>(START_VALUE);

useEffect(() => {
  const intervalId = setInterval(() => {
    setSeconds((previousSeconds) => previousSeconds - 1);
  }, 1000);

  if (seconds <= 0) {
    clearInterval(intervalId);
    functionX();
  }

  return () => clearInterval(intervalId);
}, [seconds]);

The problem with the above code is that every second the useEffect gets triggered, is there any way to access the value out of setSeconds calculation to be used inside setInterval?


Solution

  • The problem with the above code is that every second the useEffect gets triggered

    Your useEffect hook is trying to do too much. You can split the code/logic up into as many logical effect as is necessary for your use case. In any case you will have at least 1 useEffect hook being called each time the seconds state updates so the code can correctly check when it reaches 0.

    is there any way to access the value out of setSeconds calculation to be used inside setInterval?

    No, not really. In doing so you will break a few design patterns.

    Trivially you have at least a couple of options:

    Use Two useEffect Hooks to Control Interval Timer

    Use one useEffect hook with empty dependency array to initiate the interval and clean up any running intervals in the event of component unmounting, and another useEffect hook to handle the side-effect of checking when the timer expires and invoking functionX. Use a React ref to hold a reference to the timer id.

    const [seconds, setSeconds] = useState<number>(START_VALUE);
    const timerRef = useRef<number | null>(null);
    
    useEffect(() => {
      timerRef.current = setInterval(() => {
        setSeconds((seconds) => seconds - 1);
      }, 1000);
    
      return () => {
        if (timerRef.current) {
          clearInterval(timerRef.current);
        }
      };
    }, []);
    
    useEffect(() => {
      if (timerRef.current && seconds <= 0) {
        clearInterval(timerRef.current);
        timerRef.current = null;
        functionX();
      }
    }, [seconds]);
    

    Use One useEffect Hook to Control Timeout Timer

    Use a single useEffect hook and a setTimeout timer instead of an interval, so the effect running triggers the next timer iteration. This avoids creating extraneous unnecessary intervals your original code was doing.

    const [seconds, setSeconds] = useState<number>(START_VALUE);
    
    const functionX = () => console.log("timer expired");
    
    useEffect(() => {
      const intervalId = setTimeout(() => {
        setSeconds((seconds) => seconds - 1);
      }, 1000);
    
      if (seconds <= 0) {
        clearTimeout(intervalId);
        functionX();
      }
      return () => {
        clearTimeout(intervalId);
      }
    }, [seconds]);