Navigating the quirks of React's state and component model to build a countdown timer you can start, pause, resume, stop and reset. Idea is to start an interval and keep track of it's reference in a ref. On pausing said timer, we can clear the interval same way we can start a new timer on resume.
The ask goes like this:
Allow user to enter time in seconds. On click of Start
, existing timer if any must be cancelled and it should begin countdown from the newly entered time.
To start with, lets split our work into 2 parts:
- Counting down seconds
- Formatting seconds into hh:mm:ss
Counting down seconds
Let's add a form with an input and a submit button.
<form onSubmit={handleStart}>
<input
type="number"
placeholder="Enter total seconds to countdown"
ref={inputRef}
/>
<button type="submit">Start</button>
</form>
This will give us a basic form with an uncontrolled field (which is fine as we do not really need a state on user enterred value).
There is also inputRef
which is a vanilla React ref. Lets define it.
import { useRef } from "react";
export default function App() {
const inputRef = useRef();
return (
<form onSubmit={handleStart}>
<input
type="number"
placeholder="Enter total seconds to countdown"
ref={inputRef}
/>
<button type="submit">Start</button>
</form>
)
}
Now lets consider the timer. The way it works is by decrementing a count until it reaches 0
.
The only way to change a value and have it automatically updated in the DOM in React is by using state.
Let's add it below our inputRef
declaration.
// ...
const inputRef = useRef();
const [currentTime, setCurrentTime] = useState(0);
// ...
Next step is the actual countdown. We can make use of setInterval
method to execute a decrement after every second. Along with this, we need to clear a running timer before the component unmounts.
Also, as per the ask, every time our form is submitted, we need to reset the timer. So we need reference to the timerID that persists accross re-renders without causing a re-render of it's own. To that effect, we can use a ref
.
// ...
const inputRef = useRef();
const timerRef = useRef();
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
return () => {
clearInterval(timerRef.current);
};
}, []);
const startTimer = () => {
timerRef.current = setInterval(() => {
setCurrentTime((prev) => prev - 1);
}, 1000);
};
// ...
The useEffect
here returns a cleanup function that executes on unmount. Here we are clearing up any running timer.
The startTimer
function is responsible for starting a timer with a fixed delay of 1000
milliseconds and decrementing currentTime
by 1 on every tick.
Let's bring it all together in our handleStart
function which get's triggered on form submit.
Things to note:
-
handleStart
will clear any existing timers - set value of
currentTime
- trigger
startTimer
// ...
const handleStart = e => {
e.preventDefault();
if (timerRef.current) {
clearInterval(timerRef.current);
}
const secondsInput = inputRef.current.value;
setCurrentTime(() => secondsInput);
startTimer();
};
// ...
Here, const secondsInput = inputRef.current.value;
gets the current value of the input.
Formatting seconds into hh:mm:ss
Now that we have a decrementing counter, lets also add a utility to format it to hh:mm:ss
.
const secondsToHHMMSS = (seconds) => {
if (seconds < 3600)
return new Date(seconds * 1000).toISOString().substr(14, 5);
return new Date(seconds * 1000).toISOString().substr(11, 8);
};
^ is picked from this helpful stackoverflow answer: https://stackoverflow.com/a/1322771/1939344
Now, lets render it below the form:
// ...
return (
<div className="App">
<form onSubmit={handleStart}>
<input
type="number"
placeholder="Enter total seconds to countdown"
ref={inputRef}
/>
<button type="submit">Start</button>
</form>
<div className="time">{secondsToHHMMSS(currentTime)}</div>
</div>
);
// ...
Finished result:
Top comments (1)