Countdown timer with React

Countdown timer design

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:

  1. Counting down seconds

  2. 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:

  1. handleStart will clear any existing timers

  2. set value of currentTime

  3. 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: