Lifecycle
Small building blocks for effect timing (mount-only, unmount-only, update-only), declarative timers, async state, and browser scheduling APIs (requestAnimationFrame, requestIdleCallback, Web Locks, Web Workers, PerformanceObserver).
useMount
Runs effect exactly once, on mount — a thin useEffect(effect, []) wrapper for callers who want the intent to read explicitly rather than relying on an empty dependency array.
useMount(() => analytics.track("page_viewed"));useUnmount
Runs cleanup exactly once, on unmount. Always calls the latest cleanup — it doesn't need to be memoized, and doesn't need []-style discipline the way a raw useEffect cleanup would.
useUnmount(() => socket.close());useUpdateEffect
useEffect that skips the first (mount) run — only fires on dependency-driven updates, exactly like useEffect otherwise (cleanup included).
useUpdateEffect(() => {
toast(`Filter changed to ${filter}`); // never fires for the initial value
}, [filter]);useTimeout
Declarative setTimeout — schedules callback after delayMs, clearing and rescheduling when delayMs changes, and clearing on unmount. Pass delayMs: null to pause without unmounting the hook.
useTimeout(() => setShowTooltip(false), showTooltip ? 3000 : null);useInterval
Declarative setInterval — calls callback every delayMs, restarting when delayMs changes, and clearing on unmount. Pass delayMs: null to pause without unmounting the hook.
useInterval(() => setElapsed((s) => s + 1), running ? 1000 : null);useAsync
Wraps a promise-returning function with loading/error/data state. Re-runs asyncFn whenever deps changes (forwarded verbatim to the underlying effect — omit it to run once on mount). A stale run's resolution is ignored if deps changes (or the component unmounts) before it settles.
const { data, loading, error } = useAsync(() => fetchUser(id), [id]);useBeforeUnload
Registers a beforeunload handler — the classic "unsaved changes" navigation guard. Call event.preventDefault() (and, for legacy browser support, set event.returnValue = "") inside handler to trigger the browser's own confirmation prompt.
useBeforeUnload((event) => {
if (isDirty) {
event.preventDefault();
event.returnValue = "";
}
}, isDirty);useAnimationFrame
Declarative requestAnimationFrame loop — calls callback every frame with the delta time (ms) since the previous one, skipping the very first frame (no delta to report yet). Auto-cancels on unmount or when enabled becomes false.
useAnimationFrame((deltaMs) => setRotation((r) => r + deltaMs * 0.1));useIdleCallback
Wraps requestIdleCallback/cancelIdleCallback — background scheduling for low-priority work during a frame's idle time.
useIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && queue.length > 0) processNext();
});useWebLock
Wraps the Web Locks API — async mutual exclusion for a named resource, shared across same-origin tabs/workers. runExclusive(callback) runs callback once the named lock is granted, releasing it automatically when callback settles (success or throw) — this hook never leaks a held lock. supported: false is the SSR-safe default where the API doesn't exist.
const { runExclusive, status } = useWebLock("sync-cart");
const total = await runExclusive(() => mergeCartFromOtherTabs());useWorker
Offloads work to a Worker, with a promise-based run() instead of raw postMessage/onmessage plumbing. The worker is only created lazily, on the first run() — never on mount — and the same instance is reused across calls until terminate() (or unmount) tears it down.
const { run } = useWorker<number, number>(
() => new Worker(new URL("./sum.worker.ts", import.meta.url)),
);
const total = await run(42);usePerformanceObserver
Wraps PerformanceObserver — long tasks, paint timing, layout shift, and other performance entry types, streamed to callback as they happen. Subscribes on mount and whenever options changes, disconnecting the previous observer first.
usePerformanceObserver(
(list) => {
for (const entry of list.getEntries()) reportLongTask(entry);
},
{ entryTypes: ["longtask"] },
);