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);
// ...
The 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 a reference to the timerID
that persists across re-renders without causing a re-render of its 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 gets triggered on form submit.
Things to note:
handleStart
will clear any existing timersset 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, let's 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, let's consume it in our 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: