Skip to content

crup/react-timer-hook

Repository files navigation

@crup/react-timer-hook

A lightweight React hooks library for building timers, stopwatches, and real-time clocks with minimal boilerplate.

npm npm downloads CI Docs Size license types React

📚 Docs and live examples: https://crup.github.io/react-timer-hook/

Why this exists

Timers get messy when a product needs pause and resume, countdowns tied to server time, async work, or a screen full of independent rows.

@crup/react-timer-hook keeps the default import small and lets you add only the pieces your screen needs:

  • ⏱️ useTimer() from the root package for one lifecycle: stopwatch, countdown, clock, or custom flow.
  • 🔋 Add schedules, timer groups, duration helpers, and diagnostics only when a screen needs them.
  • 🧭 useTimerGroup() from /group for many keyed lifecycles with one shared scheduler.
  • 📡 useScheduledTimer() from /schedules for polling and timing context.
  • 🧩 durationParts() from /duration for common display math.
  • 🧪 Tested against rerenders, React Strict Mode, async callbacks, cleanup, and multi-timer screens.
  • 🤖 AI-ready docs are available through hosted llms.txt, llms-full.txt, and an optional MCP docs helper.

Install

npm install @crup/react-timer-hook@latest
pnpm add @crup/react-timer-hook@latest

Runtime requirements: Node 18+ and React 18+.

import { useTimer } from '@crup/react-timer-hook';
import { durationParts } from '@crup/react-timer-hook/duration';
import { useTimerGroup } from '@crup/react-timer-hook/group';
import { useScheduledTimer } from '@crup/react-timer-hook/schedules';

Live recipes

Each recipe has a live playground and a focused code sample:

Use cases

Product case Use Import Recipe
Stopwatch, call timer, workout timer Core @crup/react-timer-hook Stopwatch
Wall clock or "last updated" display Core @crup/react-timer-hook Wall clock
Auction, reservation, or job deadline Core @crup/react-timer-hook Absolute countdown
Focus timer or checkout hold that pauses Core + duration @crup/react-timer-hook + /duration Pausable countdown
OTP resend or retry cooldown Core + duration @crup/react-timer-hook + /duration OTP resend cooldown
Backend status polling Schedules @crup/react-timer-hook/schedules Polling schedule
Draft autosave or presence heartbeat Schedules @crup/react-timer-hook/schedules Autosave heartbeat
Polling that can close early Schedules @crup/react-timer-hook/schedules Poll and cancel
Auction list with independent row controls Timer group @crup/react-timer-hook/group Timer group
Checkout holds with independent controls Timer group @crup/react-timer-hook/group Checkout holds
Upload/job dashboard with per-row polling Timer group + schedules @crup/react-timer-hook/group Per-item polling
Toast expiry or runtime item timers Timer group @crup/react-timer-hook/group Toast auto-dismiss

See the full use-case guide: https://crup.github.io/react-timer-hook/use-cases/

Design assumptions and runtime limits: https://crup.github.io/react-timer-hook/project/caveats/

Quick examples

Stopwatch

import { useTimer } from '@crup/react-timer-hook';

export function Stopwatch() {
  const timer = useTimer({ updateIntervalMs: 100 });

  return (
    <>
      <output>{(timer.elapsedMilliseconds / 1000).toFixed(1)}s</output>
      <button disabled={!timer.isIdle} onClick={timer.start}>Start</button>
      <button disabled={!timer.isRunning} onClick={timer.pause}>Pause</button>
      <button disabled={!timer.isPaused} onClick={timer.resume}>Resume</button>
      <button onClick={timer.restart}>Restart</button>
    </>
  );
}

Auction countdown

Use now for wall-clock deadlines from a server, auction, reservation, or job expiry.

import { useTimer } from '@crup/react-timer-hook';

export function AuctionTimer({ auctionId, expiresAt }: {
  auctionId: string;
  expiresAt: number;
}) {
  const timer = useTimer({
    autoStart: true,
    updateIntervalMs: 1000,
    endWhen: snapshot => snapshot.now >= expiresAt,
    onEnd: () => api.closeAuction(auctionId),
  });

  const remainingMs = Math.max(0, expiresAt - timer.now);

  if (timer.isEnded) return <span>Auction ended</span>;
  return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
}

Poll and cancel early

Schedules run while the timer is active. Slow async work is skipped by default with overlap: 'skip'.

import { useScheduledTimer } from '@crup/react-timer-hook/schedules';

const timer = useScheduledTimer({
  autoStart: true,
  updateIntervalMs: 1000,
  endWhen: snapshot => snapshot.now >= expiresAt,
  schedules: [
    {
      id: 'auction-poll',
      everyMs: 5000,
      overlap: 'skip',
      callback: async (_snapshot, controls, context) => {
        console.log(`auction poll fired ${context.firedAt - context.scheduledAt}ms late`);
        const auction = await api.getAuction(auctionId);
        if (auction.status === 'sold') controls.cancel('sold');
      },
    },
  ],
});

Many independent timers

Use useTimerGroup() when every row needs its own pause, resume, cancel, restart, schedules, or onEnd.

import { useTimerGroup } from '@crup/react-timer-hook/group';

const timers = useTimerGroup({
  updateIntervalMs: 1000,
  items: auctions.map(auction => ({
    id: auction.id,
    autoStart: true,
    endWhen: snapshot => snapshot.now >= auction.expiresAt,
    onEnd: () => api.closeAuction(auction.id),
  })),
});

API reference

useTimer() settings

Key Type Required Description
autoStart boolean No Starts the lifecycle after mount. Defaults to false.
updateIntervalMs number No Render/update cadence in milliseconds. Defaults to 1000. This does not define elapsed time; elapsed time is calculated from timestamps. Use a smaller value like 100 or 20 when the UI needs finer updates.
endWhen (snapshot) => boolean No Ends the lifecycle when it returns true. Use this for countdowns, timeouts, and custom stop conditions.
onEnd (snapshot, controls) => void | Promise<void> No Called once per generation when endWhen ends the lifecycle. restart() creates a new generation.
onError (error, snapshot, controls) => void No Handles sync throws and async rejections from onEnd. Also used as the fallback for schedule callback failures when a schedule does not define onError.

useScheduledTimer() settings

Import from @crup/react-timer-hook/schedules when you need polling or scheduled side effects.

Key Type Required Description
autoStart boolean No Starts the lifecycle after mount. Defaults to false.
updateIntervalMs number No Render/update cadence in milliseconds. Defaults to 1000. Scheduled callbacks can run on their own cadence.
endWhen (snapshot) => boolean No Ends the lifecycle when it returns true.
onEnd (snapshot, controls) => void | Promise<void> No Called once per generation when endWhen ends the lifecycle.
onError (error, snapshot, controls) => void No Handles sync throws and async rejections from onEnd.
schedules TimerSchedule[] No Scheduled side effects that run while the timer is active. Async overlap defaults to skip.
diagnostics TimerDiagnostics No Optional lifecycle and schedule events. No logs are emitted unless you pass a logger.

TimerSchedule

Key Type Required Description
id string No Stable identifier used in diagnostics events and schedule context. Falls back to the array index.
everyMs number Yes Schedule cadence in milliseconds. Must be positive and finite.
leading boolean No Runs the schedule immediately when the timer starts or resumes into a new generation. Defaults to false.
overlap 'skip' | 'allow' No Controls async overlap. Defaults to skip, so a pending callback prevents another run.
callback (snapshot, controls, context) => void | Promise<void> Yes Scheduled side effect. Receives timing context with scheduledAt, firedAt, nextRunAt, overdueCount, and effectiveEveryMs.
onError (error, snapshot, controls, context) => void No Handles sync throws and async rejections from that schedule's callback. Falls back to the timer or item onError when omitted.

useTimerGroup() settings

Import from @crup/react-timer-hook/group when many keyed items need independent lifecycle control.

Key Type Required Description
updateIntervalMs number No Shared scheduler cadence for the group. Defaults to 1000.
items TimerGroupItem[] No Initial/synced timer item definitions. Each item has its own lifecycle state.
diagnostics TimerDiagnostics No Optional lifecycle and schedule events for group timers.

TimerGroupItem

Key Type Required Description
id string Yes Stable key for the item. Duplicate IDs throw.
autoStart boolean No Starts the item automatically when it is added or synced. Defaults to false.
endWhen (snapshot) => boolean No Ends that item when it returns true.
onEnd (snapshot, controls) => void | Promise<void> No Called once per item generation when that item ends naturally.
onError (error, snapshot, controls) => void No Handles sync throws and async rejections from that item's onEnd. Also used as the fallback for that item's schedule callback failures.
schedules TimerSchedule[] No Per-item schedules with the same contract as useScheduledTimer().

Values and controls

Key Type Description
status 'idle' | 'running' | 'paused' | 'ended' | 'cancelled' Current lifecycle state.
now number Wall-clock timestamp from Date.now(). Use for clocks and absolute deadlines.
tick number Number of render/update ticks produced in the current generation.
startedAt number | null Wall-clock timestamp when the current generation started.
pausedAt number | null Wall-clock timestamp for the current pause, or null.
endedAt number | null Wall-clock timestamp when endWhen ended the lifecycle.
cancelledAt number | null Wall-clock timestamp when cancel() ended the lifecycle early.
cancelReason string | null Optional reason passed to cancel(reason).
elapsedMilliseconds number Active elapsed duration calculated from monotonic time, excluding paused time.
isIdle boolean Convenience flag for status === 'idle'.
isRunning boolean Convenience flag for status === 'running'.
isPaused boolean Convenience flag for status === 'paused'.
isEnded boolean Convenience flag for status === 'ended'.
isCancelled boolean Convenience flag for status === 'cancelled'.
start() function Starts an idle timer. No-op if it is already started.
pause() function Pauses a running timer.
resume() function Resumes a paused timer from the paused elapsed value.
reset(options?) function Resets to idle and zero elapsed time. Pass { autoStart: true } to reset directly into running.
restart() function Starts a new running generation from zero elapsed time.
cancel(reason?) function Terminal early stop. Does not call onEnd.

Bundle size

The default import stays small. Add the other pieces only when that screen needs them.

Piece Import Best for Raw Gzip Brotli
⏱️ Core @crup/react-timer-hook Stopwatch, countdown, clock, custom lifecycle 4.44 kB 1.52 kB 1.40 kB
🧭 Timer group @crup/react-timer-hook/group Many independent row/item timers 10.93 kB 3.83 kB 3.50 kB
📡 Schedules @crup/react-timer-hook/schedules Polling, cadence callbacks, overdue timing context 8.62 kB 3.02 kB 2.78 kB
🧩 Duration @crup/react-timer-hook/duration days, hours, minutes, seconds, milliseconds 318 B 224 B 192 B
🔎 Diagnostics @crup/react-timer-hook/diagnostics Optional lifecycle and schedule event logging 105 B 115 B 90 B
🤖 MCP docs server react-timer-hook-mcp Optional local docs context for MCP clients and coding agents 6.95 kB 2.72 kB 2.36 kB

CI writes a size summary to the GitHub Actions UI and posts bundle-size reports on pull requests.

AI-friendly docs

Agents and docs-aware IDEs can use:

Optional local MCP docs server:

Use npx if the package is not installed in the current project:

{
  "mcpServers": {
    "react-timer-hook-docs": {
      "command": "npx",
      "args": ["-y", "@crup/react-timer-hook@latest"]
    }
  }
}

If the package is installed locally, npm also creates a bin shim in node_modules/.bin:

{
  "mcpServers": {
    "react-timer-hook-docs": {
      "command": "./node_modules/.bin/react-timer-hook-mcp",
      "args": []
    }
  }
}

The same bundled and minified server is available at node_modules/@crup/react-timer-hook/dist/mcp/server.js.

It exposes:

react-timer-hook://package
react-timer-hook://api
react-timer-hook://recipes

It also exposes MCP tools that editors are more likely to call directly:

Tool Title Description
get_api_docs Get API docs Returns compact API notes for @crup/react-timer-hook.
get_recipe Get recipe Returns guidance for a named recipe or use case.
search_docs Search docs Searches API and recipe notes for a query.

Contributing

Issues, recipes, docs improvements, and focused bug reports are welcome.

The package targets Node 18+ and React 18+.

About

A lightweight React hooks library for building timers, stopwatches, and real-time clocks with minimal boilerplate.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors