skip to Main Content

I built a countdown timer in React using useEffect and setTimeout from the following code below

import React, { useState, useEffect } from 'react'
import "./App.css";

// Countdown Timer
export default function App() {

  const [time, setTime] = useState(10);

  useEffect(() => {
    setTimeout(() => { setTime((a) => a - 1); 
    }, 1000);
  }, [time]);

  return (
    <div className="container">

      <h1>Countdown</h1>
      { time <= 0 ? <p>Time's Up!</p> : <p>{time}</p> }
    </div>
  )
}

It works fine except for the beginning, where it goes from 10 to 8..7..6..5..etc, skipping nine. I’m not sure what I should modify to get nine back.

2

Answers


  1. React in development mode re-mounts your component twice to catch bugs in things like useEffect that aren’t expecting to be mounted multiple times. See this answer for more details on why that’s the case.

    In this case, it caught a bug. The issue is that you don’t tell React how to cancel an effect if the tree hasn’t been mounted but it needs to be unmounted.

    Add a cancellation callback that calls clearTimeout and your issue will be resolved:

      useEffect(() => {
        const timerId = setTimeout(() => setTime((a) => a - 1), 1000);
        // The new bit ⬇︎
        return () => clearTimeout(timerId);
      }, [time]);
    
    Login or Signup to reply.
  2. The problem is related to the way setTimeout works in your code. When you use setTimeout inside the useEffect with a dependency on the time state, the timer is set to decrease the time by 1 second after each render. This means that when the component initially renders, the time state is 10, but the timer immediately starts and reduces it to 9 seconds.

    To be more specific, useEffect will run regardless of whether the dependencies have changed or not on the first render. After the initial run, the useEffect will continue to run each time time changes because you’ve specified [time] in the dependency array. So, after the initial run, changes to time will trigger the useEffect to execute.

    You can modify your code this way to fix the problem:

    useEffect(() => {
      const timer = setTimeout(() => {
        if (time > 0) {
          setTime((prevTime) => prevTime - 1);
        }
      }, 1000);
    
      // Clear the timer when the component unmounts or when time reaches 0
      return () => clearTimeout(timer);
    }, [time]);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search