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
Installation
npx shadcn@latest add https://craftbits.dev/r/use-timers.jsonNo 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
| Field | Type | Description |
|---|---|---|
setTimeout | (fn: () => void, ms: number, name?: string) => number | Schedule 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) => number | Schedule a repeating timer. Returns the numeric handle. Passing name clears any previous interval registered under the same name. |
clear | (handleOrName: number | string) => void | Cancel a single timer by handle or by name. No-op if unknown. |
clearAll | () => void | Cancel every pending timeout and interval registered with this hook. |
isPending | (name: string) => boolean | true while a named timer is still pending. |
Behaviour
- Auto-cleanup. Every timer is registered in an internal
Mapkeyed by handle and (optionally) by name. On unmount, the hook walks the map and calls the matchingclearTimeout/clearIntervalfor each record. - Named replacement. Calling
setTimeout(fn, ms, name)clears any earlier record undernamebefore scheduling. Debounce without a second hook — re-tap, re-tap, re-tap, only the last fires. - 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. - SSR-safe.
setTimeoutandsetIntervalare universal globals; the hook never toucheswindowat 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: reduceand short-circuit scheduling. Pair withusePrefersReducedMotion. - Auto-dismiss notifications. Pair the named auto-dismiss timer with an explicit dismiss control —
aria-liveregions should not be the only path to acknowledge the message.
Credits
- Extracted from:
algoflashcards(src/platform/hooks/ui/useTimers.ts). The source exposed anafter/every/frameAPI with type-enforced stop conditions on the interval and RAF callbacks. The library version mirrors the DOMsetTimeout/setIntervalshape (simpler signature, no RAF branch), and adds thenameparameter for debounce-style auto-replacement plus anisPending(name)check.