skip to Main Content

I’m trying to implement the countdown timer with React’s hooks. I knew, we have couple of solutions out there and thanks for that. My question is NOT How to implement timer with React's hooks BUT What is the problem with my approach

Here is my code so far.

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

const Timer = () => {
  const [minutes, setMinutes] = useState(29);
  const [seconds, setSeconds] = useState(59);
  useEffect(() => {
    const secondInterval = setInterval(() => {
      setSeconds((prev) => (prev - 1 < 0 ? 59 : prev - 1));
    }, 1000);

    const minuteInterval = setInterval(() => {
      setMinutes(minutes - 1);
    }, 60000);

    return () => {
      clearInterval(secondInterval);
      clearInterval(minuteInterval);
    };
  });

  return (
    <div>
      {minutes === 0 && seconds === 0 ? null : (
        <h1>
          {" "}
          {minutes}:{seconds < 10 ? `0${seconds}` : seconds}
        </h1>
      )}
    </div>
  );
};

export default Timer;

Basically, I just set 2 interval for every render with useEffect no depths. But the second setTimeInterval does not run and the minutes show 29 everytime.

Thanks in advance !

3

Answers


  1. When the useEffect is called (by setting the seconds for example), your intervals are cleared. This means that the minutes interval is never executed. Add an empty dependencies array to the useEffect, and set the minutes using an updater function.

    However, since your seconds and minutes are out of sync – the seconds starts from 50, and the minutes interval is 60,000 ms (60 sec), you’ll only see the minutes changing 10 seconds after the seconds counter reaches 0.

    const { useEffect, useState } = React;
    
    const Timer = () => {
      const [minutes, setMinutes] = useState(29);
      const [seconds, setSeconds] = useState(50);
      
      useEffect(() => {
        const secondInterval = setInterval(() => {
          setSeconds(prev => (prev - 1 < 0 ? 59 : prev - 1));
        }, 1000);
    
        const minuteInterval = setInterval(() => {
          setMinutes(prev => prev - 1);
        }, 60000);
    
        return () => {
          clearInterval(secondInterval);
          clearInterval(minuteInterval);
        };
      }, []);
    
      return (
        <div>
          {minutes === 0 && seconds === 0 ? null : (
            <h1>
              {" "}
              {minutes}:{seconds < 10 ? `0${seconds}` : seconds}
            </h1>
          )}
        </div>
      );
    };
    
    ReactDOM
      .createRoot(root)
      .render(<Timer />);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    You can solve this by using a second useEffect that sets the minutes whenever seconds reaches 59:

    const { useEffect, useState } = React;
    
    const Timer = () => {
      const [minutes, setMinutes] = useState(29);
      const [seconds, setSeconds] = useState(50);
      
      useEffect(() => {
        const secondInterval = setInterval(() => {
          setSeconds(prev => (prev - 1 < 0 ? 59 : prev - 1));
        }, 1000);
    
        return () => {
          clearInterval(secondInterval);
        };
      }, []);
      
      useEffect(() => {
        if(seconds === 59) setMinutes(prev => prev - 1);
      }, [seconds]);
    
      return (
        <div>
          {minutes === 0 && seconds === 0 ? null : (
            <h1>
              {" "}
              {minutes}:{seconds < 10 ? `0${seconds}` : seconds}
            </h1>
          )}
        </div>
      );
    };
    
    ReactDOM
      .createRoot(root)
      .render(<Timer />);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="root"></div>
    Login or Signup to reply.
  2. Check this article about how javascript timers work

    Since JavaScript can only ever execute one piece of code at a time (due to its single-threaded nature) each of these blocks of code are “blocking” the progress of other asynchronous events. This means that when an asynchronous event occurs (like a mouse click, a timer firing, or an XMLHttpRequest completing) it gets queued up to be executed later

    So, as JavaScript is single threaded and can only execute one line at a time, the second setTimeInterval for minutes is being blocked by first setTimeInterval used for seconds. It is getting queued up to be executed later, and since useEffect is being called for every second interval, this queue is getting cleared each time and the code never actually reaches to the second setTimeInterval

    Login or Signup to reply.
  3. Main issue with your code is missing dependency array. Without dependency array you’re adding and removing intervals every render thus your 59 seconds will never happen.

    You may want to try console logging like this to make it more clear what is error, just set some variable i globally and log it without dependency array:

      useEffect(() => {
        const secondInterval = setInterval(() => {
          setSeconds((prev) => (prev - 1 < 0 ? 59 : prev - 1))
        }, 1000)
    
        const minuteInterval = setInterval(() => {
          setMinutes(minutes - 1)
        }, 60000)
        console.log(i++)
        return () => {
          clearInterval(secondInterval)
          clearInterval(minuteInterval)
        }
      })
    

    if you do same thing with dependency array you will see that it actually only prints once which would be desired behaviour.

      useEffect(() => {
        const secondInterval = setInterval(() => {
          setSeconds((prev) => (prev - 1 < 0 ? 59 : prev - 1))
        }, 1000)
    
        const minuteInterval = setInterval(() => {
          setMinutes(minutes - 1)
        }, 60000)
        console.log(i++)
        return () => {
          clearInterval(secondInterval)
          clearInterval(minuteInterval)
        }
      }, [])
    

    you could potentially add second timer to be 600ms and then only that timer will run as your component will rerender every 600ms.

    With empty dependency array your code inside useEffect will only run once, if you wish to include some dependency (for useEffect to run once that value changes just add it to array)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search