skip to Main Content

I am trying to create a counter which decreases every second after clicking on it. The initial state of the clock is 25:00 which is rendered as {timerMinutes}:{timerSeconds} in the Timer component. I am decrementing the previous number of seconds by 1 to calculate the new state and based on this new state, the new minutes and seconds are calculated.

However, the clock goes back to 24:59 after 24:00 and keeps looping in that range forever.

Here is the code –

const Clock = () => {
  const [timerMinutes, setTimerMinutes] = useState(25);
  const [timerSeconds, setTimerSeconds] = useState(0);

  const startStop = () => {
    setInterval(()=>{
      setTimerMinutes(prevMins => {
        let time = prevMins*60;
        setTimerSeconds(prevSecs => {
          time+=prevSecs; // Total seconds of the last state
          time-=1; // The next state
          return time%60; // The number of seconds in the next state
        })
        return Math.floor(time/60);  // The number of mins in next state
      });
    }, 1000);
  }


const Timer = ({timerMinutes, timerSeconds}) => {
  return (
    <div onClick={startStop}>
      <div id="timer-label">Session</div>
      <div id="time-left">{timerMinutes<10? '0'+timerMinutes:timerMinutes}:{timerSeconds<10? '0'+timerSeconds:timerSeconds}</div>
    </div>
  );
}

  return (
    <div>
      <div>25 + 5 Clock</div>
      <Timer timerMinutes={timerMinutes} timerSeconds={timerSeconds}/>
    </div>
  );
}

Is this related to some asynchronous behavior of JS or the setInterval() function? Please dry run to see the results and correct if there are any problems in the code. Thanks!

2

Answers


  1. I think you’re making it way harder on yourself by using two states that are that closely connected. Something like this seems to work:

    const [timer, setTimer] = useState({ minutes: 25, seconds: 0});
    
    const startStop = () => {
      setInterval(()=>{
        setTimer(({ minutes, seconds }) => {
          let time = minutes*60 + seconds;
          time-=1;
    
          return {
            minutes: Math.floor(time / 60),
            seconds: time % 60,
          };
        });
      }, 1000);
    }
    

    However, you should probably not trust setInterval to keep proper timing. What you can do for a better result is probably to keep only the starting point of the timer (Date.now()) and calculate the amount of time remaining at each interval.

    The Timer component should probably not be defined inside the other one, it’s usually a bad pattern because it will unmount and remount at each render.

    Also, the fact that you are not cleaning the interval will probably cause trouble.

    Login or Signup to reply.
  2. Using modulus / math function isn’t needed.

    You should also keep the return value of setInterval so you can call clearInterval if you want to pause the timer.

    Then use a useEffect to set the interval if some value changes.

    Little demo based on your code:

    const { useState } = React;
    
    const Clock = () => {
    
        const [play, setPlay] = useState(true);
        const [timerMinutes, setTimerMinutes] = useState(25);
        const [timerSeconds, setTimerSeconds] = useState(0);
      
        const startStop = () => setPlay(p => !p);
        
        let ival = null;
      
        React.useEffect(() => {
            if (play) {
                clearInterval(ival);
            } else {
                ival = setInterval(() => {
                    if (timerSeconds === 0) {
                        setTimerSeconds(59);
                        setTimerMinutes(p => --p);
                    } else {
                        setTimerSeconds(p => --p);
                    }
                }, 1000);
            }
            return () => clearInterval(ival);
        }, [ play, timerSeconds ]);
        
        const Timer = ({timerMinutes, timerSeconds}) => {
            return (
                <div onClick={startStop}>
                    <div id="timer-label">Session</div>
                    <div id="time-left">{timerMinutes<10? '0'+timerMinutes:timerMinutes}:{timerSeconds<10? '0'+timerSeconds:timerSeconds}</div>
                </div>
            );
        }
    
        return (
            <div>
                <div>25 + 5 Clock</div>
                <Timer timerMinutes={timerMinutes} timerSeconds={timerSeconds}/>
            </div>
        );
    }
    
    ReactDOM.render(<Clock />, document.getElementById("react"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="react"></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search