useTimers

A React hook that wraps setTimeout and setInterval with automatic cleanup on unmount. The named-timer surface (a second call with the same name cancels the first) folds debounce into the same API — no extra useDebouncedCallback import for the common case.

Returns setTimeout, setInterval, clear, clearAll, and isPending(name) — safer wrappers that auto-clear on unmount and offer named timers for targeted clear.

toast pending
false
tick pending
false
tick count
0
Customize
Toast
1000 ms
Tick
500 ms

Installation

npx shadcn@latest add https://craftbits.dev/r/use-timers.json

No external dependencies. setTimeout and setInterval ship in every JavaScript runtime.

Usage

"use client";
import { useTimers } from "@craft-bits/core";
 
export function Toast() {
  const timers = useTimers();
  return (
    <button
      onClick={() => {
        showToast();
        // Re-tapping inside 1s cancels the previous hide and starts a new one.
        timers.setTimeout(() => hideToast(), 1000, "toast");
      }}
    >
      Show toast
    </button>
  );
}

A typical autoplay loop:

const timers = useTimers();
 
const start = () => timers.setInterval(advance, 600, "autoplay");
const stop = () => timers.clear("autoplay");
const running = timers.isPending("autoplay");

API

Return value

FieldTypeDescription
setTimeout(fn: () => void, ms: number, name?: string) => numberSchedule a one-shot timer. Returns the numeric handle. Passing name clears any previous timer registered under the same name.
setInterval(fn: () => void, ms: number, name?: string) => numberSchedule a repeating timer. Returns the numeric handle. Passing name clears any previous interval registered under the same name.
clear(handleOrName: number | string) => voidCancel a single timer by handle or by name. No-op if unknown.
clearAll() => voidCancel every pending timeout and interval registered with this hook.
isPending(name: string) => booleantrue while a named timer is still pending.

Behaviour

  1. Auto-cleanup. Every timer is registered in an internal Map keyed by handle and (optionally) by name. On unmount, the hook walks the map and calls the matching clearTimeout / clearInterval for each record.
  2. Named replacement. Calling setTimeout(fn, ms, name) clears any earlier record under name before scheduling. Debounce without a second hook — re-tap, re-tap, re-tap, only the last fires.
  3. Self-delete on fire. When a one-shot timer fires, the record is removed before the callback runs, so isPending(name) is false inside the callback and re-entrant scheduling under the same name does not falsely treat the firing timer as a stale registration.
  4. SSR-safe. setTimeout and setInterval are universal globals; the hook never touches window at module scope.

Examples

Debounced commit

const timers = useTimers();
 
const onChange = (value: string) => {
  setDraft(value);
  timers.setTimeout(() => commit(value), 300, "commit");
};

Each keystroke cancels the previous pending commit. Once the user pauses for 300 ms, commit(value) fires once.

Step-through animation

const timers = useTimers();
 
const playSequence = (steps: Step[]) => {
  steps.forEach((step, i) => {
    timers.setTimeout(() => runStep(step), i * 400);
  });
};

Unmounting mid-sequence aborts every queued step.

Stoppable autoplay

const timers = useTimers();
 
const start = () => timers.setInterval(advance, 600, "autoplay");
const stop = () => timers.clear("autoplay");
const running = timers.isPending("autoplay");

Props

useTimers takes no arguments. See the Return value table above for the returned UseTimersResult surface.

Accessibility

useTimers is a low-level utility — it has no DOM surface and no accessibility implications of its own. Two notes for consumers:

  • Reduced motion. When driving an autoplay sequence or step-through animation, check prefers-reduced-motion: reduce and short-circuit scheduling. Pair with usePrefersReducedMotion.
  • Auto-dismiss notifications. Pair the named auto-dismiss timer with an explicit dismiss control — aria-live regions should not be the only path to acknowledge the message.

Credits

  • Extracted from: algoflashcards (src/platform/hooks/ui/useTimers.ts). The source exposed an after / every / frame API with type-enforced stop conditions on the interval and RAF callbacks. The library version mirrors the DOM setTimeout / setInterval shape (simpler signature, no RAF branch), and adds the name parameter for debounce-style auto-replacement plus an isPending(name) check.