javascript
48 lines · 8 steps
Building a useCountdown hook in React
A custom React hook that drives a ticking countdown timer with start, pause, and reset controls.
Explained by
highlit
1import { useState, useEffect, useCallback } from 'react';
2
3export function useCountdown(initialSeconds) {
4 const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
5 const [isRunning, setIsRunning] = useState(false);
6
7 useEffect(() => {
8 if (!isRunning) return;
9
10 const id = setInterval(() => {
11 setSecondsLeft((prev) => {
12 if (prev <= 1) {
13 clearInterval(id);
14 setIsRunning(false);
15 return 0;
16 }
17 return prev - 1;
18 });
19 }, 1000);
20
21 return () => clearInterval(id);
22 }, [isRunning]);
23
24 const start = useCallback(() => {
25 setSecondsLeft((prev) => (prev > 0 ? prev : initialSeconds));
26 setIsRunning(true);
27 }, [initialSeconds]);
28
29 const pause = useCallback(() => setIsRunning(false), []);
30
31 const reset = useCallback(() => {
32 setIsRunning(false);
33 setSecondsLeft(initialSeconds);
34 }, [initialSeconds]);
35
36 const minutes = String(Math.floor(secondsLeft / 60)).padStart(2, '0');
37 const seconds = String(secondsLeft % 60).padStart(2, '0');
38
39 return {
40 secondsLeft,
41 isRunning,
42 isFinished: secondsLeft === 0,
43 display: `${minutes}:${seconds}`,
44 start,
45 pause,
46 reset,
47 };
48}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Gating a useEffect on a boolean state lets you start and stop side effects declaratively instead of imperatively.
- 2Returning a cleanup function from useEffect guarantees intervals are cleared on unmount or dependency change.
- 3A custom hook can package internal state, effects, and derived values into one clean API for components.
Related explainers
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-usecountdown-hook-in-react-explained-javascript-4214/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.