Hooks
useTimeout
A declarative one-shot setTimeout hook with a latest-ref callback, a null-to-pause delay, and reset/clear controls.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-timeout.jsonPrompt
Build a React + TypeScript "useTimeout" hook (no dependencies beyond React;
uses the browser setTimeout/clearTimeout only).
Contract
- `useTimeout(callback: () => void, delay: number | null): { reset: () => void;
clear: () => void }`.
- `delay` is in milliseconds. Passing `null` means "paused" — no timer exists
at all until `delay` becomes a number again (not just a timer that quietly
never fires).
- `reset` and `clear` are referentially stable across re-renders, so they can
safely sit in another effect's dependency array or be passed down as props.
Behavior
- The callback is stored in a ref that's updated on every render; the effect
that actually schedules the timer depends only on `delay`, never on
`callback`. This is the core difference from a bare `setTimeout` call inside
a `useEffect([callback, delay])`: consumers almost always pass an inline
arrow function that closes over changing state, and if that identity were a
dependency, every render would clear and re-schedule the timer — the delay
would keep restarting and the callback would never actually fire on time.
Here, scheduling only reacts to `delay`, while the timer that eventually
fires always calls whatever the latest callback closure is.
- On mount, and whenever `delay` changes to a non-null number: clear any
existing timer and schedule a fresh one for `delay` ms that invokes the
latest callback when it elapses.
- Whenever `delay` is (or becomes) `null`: clear any pending timer and
schedule nothing.
- `reset()`: clears the current timer (if any) and re-schedules using the
*current* `delay`, restarting the countdown from right now. If `delay` is
currently `null`, `reset()` is a no-op — there's nothing to restart.
- `clear()`: clears the current timer and does not reschedule. The timer
stays canceled until `delay` changes or `reset()` is called again.
- On unmount: the effect's cleanup clears the pending timer, so no `setState`
ever fires after the consuming component is gone.
Rendering & styling
- The hook renders nothing itself — consumers own all UI. Any visual feedback
built around it (a shrinking progress bar, a status badge, an auto-dismiss
banner) should use semantic tokens (`bg-primary`, `text-muted-foreground`,
`border`) and respect `prefers-reduced-motion` for decorative motion; the
timer itself must keep firing correctly whether or not such UI is shown.
Customization levers
- Track and return the remaining time (captured from a start timestamp) if a
consumer needs a visible countdown — deliberately left out here to keep the
contract minimal; pair this hook with a dedicated countdown hook when a
numeric readout is the actual point.
- Trigger a toast / analytics call from inside the callback itself rather than
adding an `onFire` option — the callback is already the extension point.
- Feed `delay` a computed value (message length, remaining quota, a feature
flag) instead of a literal — the hook only cares that it receives a number
or `null`, never where that number comes from.Concepts
- Latest-ref callback — the timer's scheduling effect never depends on
callback, only ondelay; a fresh inline callback every render never restarts the clock, yet the fire always invokes the newest closure. - Declarative pause via
null—delay={null}means no timer object exists at all, not a timer quietly failing to fire; this is what makes the hook safe to drive from state likeopen ? 3000 : null. - Reset restarts, it doesn't rebuild —
reset()cancels and re-schedules imperatively using whateverdelaycurrently is, without touching the effect/dependency machinery that drives automatic rescheduling. - Cancel without rebuilding —
clear()cancels the pending fire and stays canceled even thoughdelayhasn't changed; onlyreset()or an actualdelaychange re-arms it. - One-shot, not repeating — this hook fires at most once per schedule; a repeating cadence is
use-interval, and a rendered countdown display isuse-countdown.