npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@crup/react-timer-hook

v0.0.6

Published

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

Readme

@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:

  • https://crup.github.io/react-timer-hook/llms.txt
  • https://crup.github.io/react-timer-hook/llms-full.txt

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.

  • Read the docs: https://crup.github.io/react-timer-hook/
  • Open an issue: https://github.com/crup/react-timer-hook/issues
  • See the contributing guide: ./CONTRIBUTING.md
  • Release policy: https://crup.github.io/react-timer-hook/project/release-channels/

The package targets Node 18+ and React 18+.