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
const express = require('express'); const EventEmitter = require('events'); const router = express.Router();
Server-Sent Events with Express
server-sent-events
streaming
event-emitter
Advanced
8 steps
typescript
import { Injectable, signal, computed } from '@angular/core'; export type ToastKind = 'success' | 'error' | 'info' | 'warning';
Building a signal-based toast service in Angular
signals
state-management
dependency-injection
Intermediate
8 steps
javascript
import { useState, useRef } from "react"; export function TagInput({ initialTags = [], onChange }) { const [tags, setTags] = useState(initialTags);
Building a tag input in React
controlled-inputs
state-management
keyboard-handling
Intermediate
8 steps
javascript
import { NavLink, useLocation } from 'react-router-dom'; const NAV_ITEMS = [ { to: '/', label: 'Dashboard', end: true },
Building an accessible Sidebar in React
routing
accessibility
declarative-ui
Intermediate
6 steps
javascript
function escapeHtml(str) { return str.replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<',
Safely highlighting search matches in text
html-escaping
regex
search-highlighting
Intermediate
7 steps
javascript
import { useEffect, useRef } from "react"; import { useBlocker } from "react-router-dom"; export function useUnsavedChangesPrompt(isDirty, message = "You have unsaved changes. Leave anyway?") {
Guarding unsaved changes with a React hook
custom-hooks
navigation-guard
event-listeners
Intermediate
7 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.