Hooks

useThrottleValue

A generic hook that lets the first value in a window through immediately, then trickles at most one update per interval while the source keeps changing.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

/**
 * Throttles a fast-changing value with leading + trailing semantics — the
 * least-surprising default. The first `value` inside a fresh `interval`-ms
 * window is emitted right away (leading edge, deferred by a zero-delay timer
 * so the update still happens in an async callback rather than synchronously
 * during the effect); any further changes during that window are held back,
 * and the last one that arrived before the window closed is emitted once the
 * window ends (trailing edge). So the output ticks at most once every
 * `interval` ms while `value` keeps changing, and never silently drops the
 * final change the way a plain leading-only throttle would. Changing

Installation

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

Prompt

Build a React + TypeScript "useThrottleValue" hook (no dependencies beyond
React).

Contract
- `useThrottleValue<T>(value: T, interval?: number): T`.
- `interval` defaults to `200` (ms).
- Returns `throttledValue: T` — same type as the input `value`, fully
  generic. Single return value, not a tuple.

Behavior
- Leading + trailing semantics (the least-surprising default for a value
  hook): the first `value` inside a fresh `interval`-ms window is emitted
  right away; any further changes that arrive during that window are held
  back; the last value that arrived before the window closes is emitted once
  the window ends. So the output ticks at most once every `interval` ms while
  `value` keeps changing, and the final change is never silently dropped the
  way a plain leading-only throttle would drop it.
- Track the last-emitted timestamp in a ref. On every `value` change, compute
  how much of the current window has elapsed; if the window has already
  fully elapsed, schedule the next emission with (effectively) no delay, else
  schedule it for the remaining time — both paths go through the same
  trailing `setTimeout`, so the "immediate" leading case still commits state
  from an async callback rather than synchronously during the effect (keeps
  the hook safe under React's effect-purity rules; never call `Date.now()`
  during render, only inside the effect/timer callback).
- Changing `interval` takes effect on the window already in flight: recompute
  the remaining wait as `interval - elapsed` since the last emission — a
  smaller `interval` can flush the trailing edge sooner, a larger one pushes
  it later. It does not wait for a fresh window to pick up the new duration.
- The pending timer is cleared on unmount and before every re-run of the
  underlying effect, so no stray `setState` fires after the consumer is gone
  and timers never stack.

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 throttled value (e.g. `text-primary` for the
  live-updating readout, `text-muted-foreground` for the raw/source value).

Customization levers
- `interval` — the rate-limit window in ms; smaller for a snappier trickle,
  larger for expensive downstream work (layout, network, heavy recompute).
- Leading/trailing toggle — expose `{ leading?: boolean; trailing?: boolean }`
  as an opt-in extension if a consumer wants leading-only (fire on entry,
  ignore the rest of the window) or trailing-only (classic debounce-like
  settle, but at a fixed cadence instead of on silence). Leading+trailing
  stays the default because it is the case nobody has to reason about.
- Pair with a `range-slider`/drag-driven component: feed its raw, per-frame
  value into this hook and use `throttledValue` for expensive derived work
  (previews, requests) while the control itself still renders every frame
  off the raw value.

Concepts

  • Leading + trailing, not leading-only — a naive throttle only fires on the leading edge of each window and drops whatever happened after; this hook also commits the last value the window saw, so a drag that ends mid-window is never lost — you always get the final position, just delayed by at most interval ms.
  • Fixed-rate window, not a restart-on-change timer — this is the core distinction from debounce. Throttle's window boundary is anchored to the last emission, not to the last change: continuous changes keep landing inside the same window and get coalesced, but the window itself keeps closing on schedule. Debounce instead restarts its timer on every change, so continuous changes push the result further away and it never fires until things go quiet.
  • Throttle vs debounce (disambiguation) — throttle guarantees a steady trickle of updates during continuous activity (scroll position, drag coordinates, live previews); debounce guarantees a single settled result after activity stops (search queries, autosave). Reach for throttle when the user needs to see progress while they're still interacting; reach for debounce when only the final answer matters.
  • Async commit, not synchronous effect writes — both the "immediate" leading emission and the delayed trailing one go through the same setTimeout callback, so state is always committed from an async callback rather than synchronously inside the effect body.
  • Cleanup on every path — the pending 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