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
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:The problem is related to the way
setTimeout
works in your code. When you usesetTimeout
inside theuseEffect
with a dependency on thetime
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, theuseEffect
will continue to run each timetime
changes because you’ve specified[time]
in the dependency array. So, after the initial run, changes totime
will trigger theuseEffect
to execute.You can modify your code this way to fix the problem: