'error in implementing seconds countdown using state in react
I have the following react component where I am trying to start a countdown from 30 to 0 as follows:
import React from 'react';
function Countdown () {
const [time, setTime] = React.useState('');
const [start, setStart] = React.useState(false);
const startTimer = () => {
setStart(true)
setTime('30')
}
const startTiming = setInterval(() => {
if (start) {
if (time !== '0') {
setTime((parseInt(time) - 1).toString());
} else {
clearInterval(startTiming)
setStart(false);
}
}
}, 1000)
return (<>
{ start ? time : 'wait to start' }
<button onClick={startTimer}>Start Timer</button>
</>)
}
export default Countdown;
I want to start the countdown from 30 seconds by clicking the button. However, once the countdown gets to about 23 seconds, it no longer counts down consistently by 1 second and instead seems to go into an infinite loop - would anyone know why this is? Ideally I'd like to implement this by using the start state.
Solution 1:[1]
I would suggest a useTimer(hz) hook that captures an offset when the user clicks Start. This way elapsed time is always correct and doesn't drift due to imperfect timeout deltas. The hook returns [elapsed, { start, stop }] which is the total elapsed time with floating point precision and start and stop controls. The optional hz parameter controls the frequency at which the timer updates.
The Countdown component has seconds as well as a configurable hz property. By subtracting elapsed time from seconds, we know how much time is remaining.
Run the program below and click Start on each timer and don't forget to have a good time!
function useTimer(hz = 100) {
const [[offset, now], setState] = React.useState([null, null])
const start = React.useCallback(_ => setState([Date.now(), Date.now()]))
const stop = React.useCallback(_ => setState([null, null]))
React.useEffect(_ => {
if (offset == null) return
const t = setTimeout(_ => { setState([offset, Date.now()]) }, 1000 / hz)
return _ => clearTimeout(t)
}, [offset, now])
return [offset == null ? 0 : (now - offset) / 1000, {start, stop}]
}
function Countdown({ seconds, hz = 1 }) {
const [elapsed, { start, stop }] = useTimer(hz)
React.useEffect(_ => { if (elapsed > seconds) stop() }, [elapsed, stop])
return <div>
<pre>{`<Countdown seconds={${seconds}} hz={${hz}} />`}</pre>
{ elapsed > seconds
? 0
: (seconds - elapsed).toFixed(2)
}
{ elapsed > 0
? <button type="button" onClick={stop} children="Reset" />
: <button type="button" onClick={start} children="Start" />
}
</div>
}
ReactDOM.render([
<Countdown seconds={30} />,
<Countdown seconds={20} hz={10} />,
<Countdown seconds={10} hz={100} />
], document.querySelector("#app"))
body { font-family: monospace; }
pre { background-color: #ffe; padding: 0.25rem 0; margin: 0; }
p { margin: 0; }
div { margin: 0 0 1rem; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
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 | ??? |
