Hooks
useDebounceValue
A generic hook that returns a value only after it has stopped changing for a delay, with no flash on first render.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-debounce-value.jsonPrompt
Build a React + TypeScript "useDebounceValue" hook (no dependencies beyond
React).
Contract
- `useDebounceValue<T>(value: T, delay?: number): T`.
- `delay` defaults to `300` (ms).
- Returns `debouncedValue: T` — same type as the input `value`, fully generic.
- Single return value, not a tuple. Keep the signature minimal; expose
`flush`/`cancel` only as an opt-in extension (see levers), not by default.
Behavior
- On mount, the hook returns the initial `value` immediately — no flash to an
empty/default state before the first debounce window elapses.
- Every time `value` changes, start (or restart) a `delay`-ms timer. If
`value` changes again before the timer fires, the previous timer is
discarded and a new one starts from zero — only the value that survives a
full quiet period of `delay` ms gets committed to `debouncedValue`.
- Changing `delay` itself also restarts the wait using the new duration.
- On unmount, the pending timer is cleared so no `setState` fires after the
consuming component is gone. The timer is also cleared before every re-run
of the underlying effect (i.e. before starting the next one), so timers
never stack.
- The `setState` that commits the debounced value happens inside the
`setTimeout` callback (an async, event-driven callback), never synchronously
in the effect body — this keeps the hook safe under React's strict
effect-purity rules.
Rendering & styling
- The hook renders nothing itself and touches no DOM/CSS — it is pure state
logic. Consumers own all UI and should use semantic tokens for any visual
feedback that depends on the debounced value (e.g. `text-muted-foreground`
for a pending/stale indicator).
Customization levers
- `delay` — the silence window in ms; smaller for snappier UI, larger for
expensive downstream work (network requests, heavy filtering).
- `maxWait` — a ceiling that forces a commit even under continuous changes,
if a consumer needs "at most N ms of staleness" instead of pure debounce.
Deliberately NOT included by default to keep the contract simple.
- Pair with a `search-input` component: feed its raw `value` into this hook
and use `debouncedValue` as the actual filter/query trigger.Concepts
- Debounced value, not debounced callback — this hook has value semantics: it hands back a settled
T, so any consumer (filter, effect, memo) just reads the latest committed value. A debounced callback (e.g.lodash.debounce(fn)) has function semantics — it controls when a function runs, including trickythis/argument-identity and cancellation concerns. Wrapping a callback library into a hook fights React's render model; tracking a value in state is idiomatic React and composes withuseEffect/useMemodirectly. - Silence window, not a fixed delay — "debounce" means "wait for
delayms of no further changes," not "waitdelayms after the first change." Every newvaluerestarts the clock, so a user who keeps typing never sees an update until they actually pause. - Debounce vs throttle (disambiguation) — debounce commits only after the input goes quiet; throttle commits at most once per fixed interval regardless of how continuously the input changes. Use this hook when you want to react to the final value of a burst (search query, resize end); use throttle when you want a steady trickle of updates during continuous activity (scroll position, drag coordinates).
- First-frame value, no flash —
debouncedValueis initialized to the incomingvalue, so there's no artificial "empty" or "loading" state on first render — only subsequent changes go through the delay. - Cleanup on every path — the timer is cleared both on unmount and before each new timer starts, so rapid changes never leave orphaned timers stacking
setStatecalls.