useCountdown

Medium

Prompt

Build a React hook called useCountdown that runs a countdown from a starting number of seconds down to zero.

const { isRunning, start, stop, seconds } =
useCountdown(totalTime);

What the hook returns:

  • seconds: the current number of seconds left. Starts at totalTime.
  • isRunning: true while the countdown is active, false otherwise.
  • start: a function that begins the countdown.
  • stop: a function that ends the countdown and puts seconds back to totalTime.

How the countdown behaves:

  • At the start, the countdown is idle: seconds is totalTime and isRunning is false.
  • After calling start(), seconds drops by 1 every second until it reaches zero.
  • If stop() is called at any point, the countdown ends right away and seconds returns to totalTime.
  • When the countdown runs all the way to zero on its own, the hook behaves as if stop() was called: isRunning becomes false and seconds is reset to totalTime.

What the demo page should show:

  • Two buttons: one labeled Start, one labeled Stop.
  • While the countdown is running, display the current value of seconds.
  • While the countdown is idle, display the text No Timer Running.

Playground

Hint 1

You need two pieces of state: one for the current seconds, one for whether the timer is running. Pick sensible initial values for each.

Hint 2

Put the tick logic inside a useEffect that depends on isRunning and seconds. Return early when isRunning is false so no timer is scheduled. Otherwise, schedule a single setTimeout for 1 second from now.

Hint 3

Inside the timeout callback, check the current seconds. If it is 1 or less, this is the last tick: reset seconds to totalTime and flip isRunning to false. Otherwise, just subtract 1 from seconds.

Hint 4

Return a cleanup function from your useEffect that calls clearTimeout. Without cleanup, every render schedules a new timeout without canceling the previous one, which makes the timer overlap and misbehave.

Solution

Explanation

I will walk you through the useCountdown question which is commonly asked at Walmart. We will go through the solution step by step and I will explain what is happening at each step so even if you are new to React, you will be able to follow along.

Before we start writing code, let us understand what we are building. We need a React hook that keeps track of two things: how many seconds are left, and whether the countdown is running. When the user calls start, we begin counting down one second at a time. When they call stop, we end the countdown and put the seconds back to the starting value. When the countdown reaches 0 on its own, we reset everything back.

Now, let's start solving.

Setting Up the State

We will use two useState calls. One for seconds and one for isRunning.

hooks/useCountdown.js

import { useState } from 'react';
export function useCountdown(totalTime) {
const [seconds, setSeconds] = useState(totalTime);
const [isRunning, setIsRunning] = useState(false);
}

seconds starts at totalTime, which is the number the user passed in. isRunning starts at false because nothing should tick until the user presses Start.

Ticking and Resetting with useEffect

Now we need to tick every second while isRunning is true, and reset when the countdown reaches 0. We will do both in a single useEffect that schedules one setTimeout per tick.

hooks/useCountdown.js

import { useState, useEffect } from 'react';
export function useCountdown(totalTime) {
const [seconds, setSeconds] = useState(totalTime);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return;
const id = setTimeout(() => {
if (seconds <= 1) {
setSeconds(totalTime);
setIsRunning(false);
} else {
setSeconds(seconds - 1);
}
}, 1000);
return () => clearTimeout(id);
}, [isRunning, seconds, totalTime]);
}

Here is what this effect does:

  • If isRunning is false, we return right away and schedule nothing.
  • Otherwise, we schedule a single setTimeout for 1 second from now.
  • When the timeout fires, we check the current seconds. If it is 1 or lower, this is the last tick: we reset seconds to totalTime and flip isRunning to false. Otherwise, we subtract 1 from seconds.
  • The cleanup function clears any pending timeout. It runs when the component unmounts, when isRunning flips, or when seconds changes.

The effect depends on seconds, so every time seconds changes the effect re-runs: the old timeout is cleared and a fresh one is scheduled for the next tick. This is how we get a chain of 1-second ticks that keeps going until the reset happens.

Why setTimeout instead of setInterval

If we used setInterval, its callback would read seconds from the render when the interval was set up, and that value would stay frozen even on later renders. To work around it, we would have to use setSeconds((s) => s - 1) (the updater form), and then the reset logic becomes awkward because we cannot cleanly call setIsRunning from inside a state updater.

With setTimeout inside a useEffect that depends on seconds, the effect re-runs after every tick. The callback always sees the newest seconds, so we can check it directly and decide whether to tick or reset.

Exposing the API

The last piece is to return the four values the hook's user expects, plus two small handler functions for start and stop.

hooks/useCountdown.js

import { useState, useEffect } from 'react';
export function useCountdown(totalTime) {
const [seconds, setSeconds] = useState(totalTime);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return;
const id = setTimeout(() => {
if (seconds <= 1) {
setSeconds(totalTime);
setIsRunning(false);
} else {
setSeconds(seconds - 1);
}
}, 1000);
return () => clearTimeout(id);
}, [isRunning, seconds, totalTime]);
const start = () => setIsRunning(true);
const stop = () => {
setIsRunning(false);
setSeconds(totalTime);
};
return { isRunning, start, stop, seconds };
}

start flips isRunning to true, which makes the effect run and schedule the first tick. stop does two things at once: it flips isRunning back to false (so the effect returns early and the pending timeout is cleared) and resets seconds to totalTime.

Walking Through an Example

Let us trace what happens when totalTime is 3 and the user clicks Start.

  1. On first render, seconds = 3 and isRunning = false. The effect returns early and nothing is scheduled.
  2. User clicks Start. setIsRunning(true) runs.
  3. Re-render with isRunning = true. The effect runs and schedules a setTimeout for 1 second. Screen shows 3s.
  4. After 1 second, the timeout fires. seconds is 3, not 1, so setSeconds(2) runs.
  5. Re-render with seconds = 2. The effect re-runs, cleans up the old timeout (already fired, no-op), and schedules a new one. Screen shows 2s.
  6. After 1 second, the timeout fires. seconds is 2, so setSeconds(1) runs.
  7. Re-render with seconds = 1. Another timeout is scheduled. Screen shows 1s.
  8. After 1 second, the timeout fires. seconds is 1, so the reset branch runs: setSeconds(totalTime) and setIsRunning(false).
  9. Re-render with isRunning = false and seconds = 3. The effect returns early. The demo now shows "No Timer Running".

Common Pitfalls

A common mistake is skipping the cleanup function. Without it, every render schedules a new timeout without canceling the previous one, so you end up with overlapping timers. After a few ticks, seconds jumps around in weird ways. Always return a cleanup function from useEffect when you set up a timeout, interval, or any other side effect.

Communicate While You Code

During the interview, explain your thought process out loud to the interviewer. For example:

  • "I am using two pieces of state: seconds for the countdown and isRunning for the on/off flag."
  • "I am using setTimeout instead of setInterval, and I include seconds in the effect's dependency array. That way the effect re-runs each tick and the callback always sees the freshest seconds."
  • "Inside the timeout, I check if this is the last tick. If so, I reset seconds to totalTime and flip isRunning to false. Otherwise I just subtract 1."
  • "I am returning a cleanup function to clear the pending timeout when the effect re-runs or the component unmounts."

Interviewers appreciate candidates who can explain why they chose a particular approach, not just what the code does.