Skip to content

Repository files navigation

@crup/react-timer-hook

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

npmnpm downloadsCIDocsSizelicensetypesReact

📚 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 caseUseImportRecipe
Stopwatch, call timer, workout timerCore@crup/react-timer-hookStopwatch
Wall clock or "last updated" displayCore@crup/react-timer-hookWall clock
Auction, reservation, or job deadlineCore@crup/react-timer-hookAbsolute countdown
Focus timer or checkout hold that pausesCore + duration@crup/react-timer-hook + /durationPausable countdown
OTP resend or retry cooldownCore + duration@crup/react-timer-hook + /durationOTP resend cooldown
Backend status pollingSchedules@crup/react-timer-hook/schedulesPolling schedule
Draft autosave or presence heartbeatSchedules@crup/react-timer-hook/schedulesAutosave heartbeat
Polling that can close earlySchedules@crup/react-timer-hook/schedulesPoll and cancel
Auction list with independent row controlsTimer group@crup/react-timer-hook/groupTimer group
Checkout holds with independent controlsTimer group@crup/react-timer-hook/groupCheckout holds
Upload/job dashboard with per-row pollingTimer group + schedules@crup/react-timer-hook/groupPer-item polling
Toast expiry or runtime item timersTimer group@crup/react-timer-hook/groupToast 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';exportfunctionStopwatch(){consttimer=useTimer({updateIntervalMs: 100});return(<><output>{(timer.elapsedMilliseconds/1000).toFixed(1)}s</output><buttondisabled={!timer.isIdle}onClick={timer.start}>Start</button><buttondisabled={!timer.isRunning}onClick={timer.pause}>Pause</button><buttondisabled={!timer.isPaused}onClick={timer.resume}>Resume</button><buttononClick={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';exportfunctionAuctionTimer({ auctionId, expiresAt }: {auctionId: string;expiresAt: number;}){consttimer=useTimer({autoStart: true,updateIntervalMs: 1000,endWhen: snapshot=>snapshot.now>=expiresAt,onEnd: ()=>api.closeAuction(auctionId),});constremainingMs=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';consttimer=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`);constauction=awaitapi.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';consttimers=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

KeyTypeRequiredDescription
autoStartbooleanNoStarts the lifecycle after mount. Defaults to false.
updateIntervalMsnumberNoRender/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) => booleanNoEnds the lifecycle when it returns true. Use this for countdowns, timeouts, and custom stop conditions.
onEnd(snapshot, controls) => void | Promise<void>NoCalled once per generation when endWhen ends the lifecycle. restart() creates a new generation.
onError(error, snapshot, controls) => voidNoHandles 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.

KeyTypeRequiredDescription
autoStartbooleanNoStarts the lifecycle after mount. Defaults to false.
updateIntervalMsnumberNoRender/update cadence in milliseconds. Defaults to 1000. Scheduled callbacks can run on their own cadence.
endWhen(snapshot) => booleanNoEnds the lifecycle when it returns true.
onEnd(snapshot, controls) => void | Promise<void>NoCalled once per generation when endWhen ends the lifecycle.
onError(error, snapshot, controls) => voidNoHandles sync throws and async rejections from onEnd.
schedulesTimerSchedule[]NoScheduled side effects that run while the timer is active. Async overlap defaults to skip.
diagnosticsTimerDiagnosticsNoOptional lifecycle and schedule events. No logs are emitted unless you pass a logger.

TimerSchedule

KeyTypeRequiredDescription
idstringNoStable identifier used in diagnostics events and schedule context. Falls back to the array index.
everyMsnumberYesSchedule cadence in milliseconds. Must be positive and finite.
leadingbooleanNoRuns the schedule immediately when the timer starts or resumes into a new generation. Defaults to false.
overlap'skip' | 'allow'NoControls async overlap. Defaults to skip, so a pending callback prevents another run.
callback(snapshot, controls, context) => void | Promise<void>YesScheduled side effect. Receives timing context with scheduledAt, firedAt, nextRunAt, overdueCount, and effectiveEveryMs.
onError(error, snapshot, controls, context) => voidNoHandles 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.

KeyTypeRequiredDescription
updateIntervalMsnumberNoShared scheduler cadence for the group. Defaults to 1000.
itemsTimerGroupItem[]NoInitial/synced timer item definitions. Each item has its own lifecycle state.
diagnosticsTimerDiagnosticsNoOptional lifecycle and schedule events for group timers.

TimerGroupItem

KeyTypeRequiredDescription
idstringYesStable key for the item. Duplicate IDs throw.
autoStartbooleanNoStarts the item automatically when it is added or synced. Defaults to false.
endWhen(snapshot) => booleanNoEnds that item when it returns true.
onEnd(snapshot, controls) => void | Promise<void>NoCalled once per item generation when that item ends naturally.
onError(error, snapshot, controls) => voidNoHandles sync throws and async rejections from that item's onEnd. Also used as the fallback for that item's schedule callback failures.
schedulesTimerSchedule[]NoPer-item schedules with the same contract as useScheduledTimer().

Values and controls

KeyTypeDescription
status'idle' | 'running' | 'paused' | 'ended' | 'cancelled'Current lifecycle state.
nownumberWall-clock timestamp from Date.now(). Use for clocks and absolute deadlines.
ticknumberNumber of render/update ticks produced in the current generation.
startedAtnumber | nullWall-clock timestamp when the current generation started.
pausedAtnumber | nullWall-clock timestamp for the current pause, or null.
endedAtnumber | nullWall-clock timestamp when endWhen ended the lifecycle.
cancelledAtnumber | nullWall-clock timestamp when cancel() ended the lifecycle early.
cancelReasonstring | nullOptional reason passed to cancel(reason).
elapsedMillisecondsnumberActive elapsed duration calculated from monotonic time, excluding paused time.
isIdlebooleanConvenience flag for status === 'idle'.
isRunningbooleanConvenience flag for status === 'running'.
isPausedbooleanConvenience flag for status === 'paused'.
isEndedbooleanConvenience flag for status === 'ended'.
isCancelledbooleanConvenience flag for status === 'cancelled'.
start()functionStarts an idle timer. No-op if it is already started.
pause()functionPauses a running timer.
resume()functionResumes a paused timer from the paused elapsed value.
reset(options?)functionResets to idle and zero elapsed time. Pass { autoStart: true } to reset directly into running.
restart()functionStarts a new running generation from zero elapsed time.
cancel(reason?)functionTerminal early stop. Does not call onEnd.

Bundle size

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

PieceImportBest forRawGzipBrotli
⏱️ Core@crup/react-timer-hookStopwatch, countdown, clock, custom lifecycle4.44 kB1.52 kB1.40 kB
🧭 Timer group@crup/react-timer-hook/groupMany independent row/item timers10.93 kB3.83 kB3.50 kB
📡 Schedules@crup/react-timer-hook/schedulesPolling, cadence callbacks, overdue timing context8.62 kB3.02 kB2.78 kB
🧩 Duration@crup/react-timer-hook/durationdays, hours, minutes, seconds, milliseconds318 B224 B192 B
🔎 Diagnostics@crup/react-timer-hook/diagnosticsOptional lifecycle and schedule event logging105 B115 B90 B
🤖 MCP docs serverreact-timer-hook-mcpOptional local docs context for MCP clients and coding agents6.95 kB2.72 kB2.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:

ToolTitleDescription
get_api_docsGet API docsReturns compact API notes for @crup/react-timer-hook.
get_recipeGet recipeReturns guidance for a named recipe or use case.
search_docsSearch docsSearches 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

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages