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…

"use client"

import * as React from "react"

/**
 * Debounces a fast-changing value. The first render returns `value` as-is
 * (no flash-to-empty), then `debouncedValue` only catches up to the latest
 * `value` once `delay` ms have passed without `value` changing again — every
 * new `value` restarts the wait. Changing `delay` also restarts the timer
 * with the new duration. The pending timer is cleared on unmount (and before
 * every re-run), so no stray `setState` fires after the consumer is gone.
 */
export function useDebounceValue<T>(value: T, delay = 300): T {
  const [debouncedValue, setDebouncedValue] = React.useState(value)

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/use-debounce-value.json

Prompt

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 tricky this/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 with useEffect/useMemo directly.
  • Silence window, not a fixed delay — "debounce" means "wait for delay ms of no further changes," not "wait delay ms after the first change." Every new value restarts 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 flashdebouncedValue is initialized to the incoming value, 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 setState calls.

On This Page