Countdown Timer
Create a countdown timer where users can set a duration, and it counts down to zero with updates displayed visually.
Solution
Explanation
The HTML
Structurally what you see is what you get. An input
, button
and div
for the display.
Starting the timer
useRef
is an efficient way to get the value from the input.
The handleStart
function, will parse the input as a number, then we have a count
state to track the value.
Mimicking a timer
Now we can set the count
value, we need to make the timer count down in seconds.
The useEffect
function allows us to update the counter when the count
state gets updated.
The intervalRef
variable keeps a setInterval
reference to allow us to update and clear. The if statements contain a lot of checks that the value is valid and that there are no old values still around.
The fundamental element to make this function is the setCount
function that takes the current count and reduces it by 1.
Disclaimer: using setInterval
is not an accurate way to measure time but works well in simple cases.
Cleanup
You'll notice the the many areas where we check the status of the setInterval ref
. We mentioned the check to see if a ref exists it needs to be cleared first. But there is also a return
statement in the useEffect
that is called a cleanup function
. This makes sure that when we leave this page and the component unmounts, if there is still an interval present, it is cleared.
Code
import { useRef, useState, useEffect } from "react";
import styles from "./Solution.module.css";
const Solution = () => {
const inputRef = useRef<null | HTMLInputElement>(null);
const intervalRef = useRef<number | null>(null);
const [count, setCount] = useState(0);
const handleStart = () => {
const inputValue = inputRef.current?.value;
const numberValue = inputValue ? parseInt(inputValue, 10) : 0;
setCount(numberValue); // Set count and start/restart timer
};
// this was required for multiple start presses
useEffect(() => {
// Only start a new timer if count is greater than 0
if (count > 0) {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current); // Clear existing timer
}
intervalRef.current = setInterval(() => {
setCount((prevCount) => (prevCount > 0 ? prevCount - 1 : 0));
}, 1000) as unknown as number;
} else if (intervalRef.current) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
return () => {
// Cleanup interval on component unmount or count changes
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
}
};
}, [count]); // Effect depends on count
return (
<>
<label className={styles.label} htmlFor="time">
Set time in seconds
</label>
<input
ref={inputRef}
type="number"
id="time"
className={styles.timeInput}
defaultValue={0}
/>
<button onClick={handleStart} type="button" className={styles.button}>
Start
</button>
<div className={styles.display}>{count}</div>
</>
);
};
export default Solution;
Styling
.timeInput,
.button,
.display {
padding: 10px;
margin: 5px 0;
}
.label {
display: block;
}
.timeInput {
display: inline-block;
width: 60px;
margin-right: 5px;
}
.button {
display: inline;
cursor: pointer;
}
.display {
display: block;
width: 60px;
border: 1px solid gray;
}