'React Timer value get struck after it reaches 12 sec

I tried to create a react timer that counts 20 seconds and then stops at 0 seconds. But the problem is it gets struck randomly in between 14,13 or 12 seconds and they keep repeating the same value again and again. Here is my code.

    import React, { useEffect,useState } from 'react';

    const Airdrop = () => {

    const [timer,setTimer] = useState(0);
    const [time,setTime] = useState({});
    const [seconds,setSeconds] = useState(20);


    const startTimer = () =>{
        if(timer === 0 && seconds > 0){
            setInterval(countDown,1000);
        }
    }

    const countDown = ()=>{
        let secondsValue = seconds - 1;
        let timeValue = secondsToTime(secondsValue);
        setTime(timeValue);
        setSeconds(secondsValue);
        if(secondsValue === 0){
            clearInterval(timer);
        }
    }

    const secondsToTime = (secs)=>{
        let hours,minutes,seconds;
        hours = Math.floor(secs/(60*60));
        let devisor_for_minutes = secs % (60*60);
        minutes = Math.floor(devisor_for_minutes/60);
        let devisor_for_seconds = devisor_for_minutes % 60;
        seconds = Math.ceil(devisor_for_seconds);
        let obj = {
            "h": hours,
            "m": minutes,
            "s": seconds
        }
        return obj;
    }

    useEffect(() => {
        let timeLeftVar = secondsToTime(seconds);
        setTime(timeLeftVar);
    }, []);

    useEffect(() => {
        startTimer();
    });

    return (
        <div style={{color:"black"}}>
            {time.m}:{time.s}
            
        </div>
    )
}

export default Airdrop


Solution 1:[1]

When you are using multiple state and they each depend on each other it is more appropriate to use a reducer because state updates are asynchronous. You might update one state based on a stale value.

However in your case, we don't need a reducer because we can derive all the data we need from a single state.

When you set the state, it is not immediately updated and it might cause issues when the next state depend on the last one. Especially when you use it like this:

const newState = state-1;
setState(newState);

However with functional updates we can directly use the last state to set the next one.

setState((prevState)=> prevState-1);

I took the liberty of creating helper functions to make your component leaner. You can copy them in a file in /helpers and import them directly.

I also replaced the interval with a timeout because it is easy get a situation where we don't clear the interval. For example the component is unmounted and without finishing the timer. We then get an interval running indefinitely.

import React, { useEffect, useState } from 'react';

const getHours = (duration) => {
    const hours = Math.floor(duration / 3600);
    if (hours < 10) return '0' + hours.toString();
    return hours.toString();
};
const getMinutes = (duration) => {
    const minutes = Math.floor((duration - +getHours(duration) * 3600) / 60);
    if (minutes < 10) return '0' + minutes.toString();
    return minutes.toString();
};
const getSeconds = (duration) => {
    const seconds =
        duration - +getHours(duration) * 3600 - +getMinutes(duration) * 60;
    if (seconds < 10) return '0' + seconds.toString();
    return seconds.toString();
};

const Airdrop = (props) => {
    const { duration = 20 } = props;
    const [seconds, setSeconds] = useState(duration);

    useEffect(() => {
        if (seconds === 0) return;
        const timeOut = setTimeout(() => {
            setSeconds((prevSeconds) => {
                if (prevSeconds === 0) return 0;
                return prevSeconds - 1;
            });
        }, 1000);
        return () => {
            clearTimeout(timeOut);
        };
    }, [seconds]);

    return (
        <div style={{ color: 'black' }}>
            {getHours(seconds)}:{getMinutes(seconds)}:{getSeconds(seconds)}
        </div>
    );
};
export default Airdrop;

Solution 2:[2]

You are creating a new interval each time the component is rendered because one of the useEffect hooks is missing a dependency array.

const startTimer = () => {
  if (timer === 0 && seconds > 0) {
    setInterval(countDown,1000);
  }
}

useEffect(() => {
  startTimer();
});

I suggest the following:

  • Capture the timer id in a React ref, use a mounting useEffect hook to return a cleanup function to clear any running intervals if/when component unmounts
  • Remove the time state, it is derived state from the seconds state, just compute it each render cycle
  • Add a dependency to the useEffect hook starting the timer
  • Move secondsToTime utility function outside component

Code

const secondsToTime = (secs) => {
  let hours, minutes, seconds;
  hours = Math.floor(secs / (60 * 60));
  let devisor_for_minutes = secs % (60 * 60);
  minutes = Math.floor(devisor_for_minutes / 60);
  let devisor_for_seconds = devisor_for_minutes % 60;
  seconds = Math.ceil(devisor_for_seconds);
  return {
    h: hours,
    m: minutes,
    s: seconds
  };
};

...

const Airdrop = () => {
  const timerRef = useRef(null);
  const [seconds, setSeconds] = useState(20);

  useEffect(() => {
    return () => clearInterval(timerRef.current); // <-- clean up any running interval on unmount
  }, []);

  const startTimer = () => {
    if (timerRef.current === null && seconds > 0) {
      timerRef.current = setInterval(countDown, 1000); // <-- capture timer id to stop interval
    }
  };

  const countDown = () => {
    setSeconds((seconds) => seconds - 1); // <-- interval callback only decrement time
  };

  useEffect(() => {
    if (seconds === 0) {
      clearInterval(timerRef.current);
      timerRef.current = null;
      setSeconds(20);
    }
  }, [seconds]); // <-- check seconds remaining to kill interval/reset

  useEffect(() => {
    startTimer();
  }, []); // <-- only start timer once

  const time = secondsToTime(seconds); // <-- compute time

  return (
    <div style={{ color: "black" }}>
      {time.m}:{time.s}
    </div>
  );
};

Edit react-timer-value-get-struck-after-it-reaches-12-sec

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2