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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Gating a useEffect on a boolean state lets you start and stop side effects declaratively instead of imperatively.
  2. 2Returning a cleanup function from useEffect guarantees intervals are cleared on unmount or dependency change.
  3. 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) => ({
    '&': '&amp;',
    '<': '&lt;',

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.

Building a useCountdown hook in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code