javascript 91 lines · 3 tabs

Debounced Search Input in React with a Reusable useDebouncedValue Hook

Shared by codesnips Jul 2026
3 tabs
import { useEffect, useState } from "react";

export function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => {
      setDebounced(value);
    }, delay);

    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
3 files · javascript Explain with highlit

This snippet shows the standard way to keep a typeahead search from firing a network request on every keystroke: the input state updates immediately for a responsive UI, but the value that drives the query is delayed until the user pauses typing. The logic is split into a small reusable hook and the component that consumes it.

In useDebouncedValue hook, the pattern is deliberately simple: it takes a live value and a delay, holds a separate debounced state, and starts a setTimeout inside a useEffect keyed on [value, delay]. Every time value changes, the effect's cleanup function runs and calls clearTimeout, cancelling the pending update before scheduling a fresh one. Only when the value stops changing for the full delay does the timer survive to commit the new debounced value. This cleanup-based cancellation is what makes debouncing in React feel natural — the effect lifecycle does the bookkeeping instead of a manually tracked timer ref.

The second hook, useSearch hook, layers async concerns on top. It debounces the raw query via useDebouncedValue, then runs a fetch whenever the debounced term changes. It uses an AbortController so that a stale in-flight request is aborted when a newer search starts or the component unmounts; the AbortError is swallowed on purpose because a cancelled request is expected, not a failure. It also trims the term and short-circuits empty input to avoid pointless requests, and tracks loading and error so the UI can reflect state.

SearchBox component wires it together. The <input> is fully controlled by query, giving instant feedback, while the results list reflects only the settled debounced term. Because the debounced value lags the input, the component can show a subtle loading indicator during the gap.

The main trade-off is latency versus request volume: a longer delay cuts backend load but makes results feel slower. Around 300ms is a common compromise. A pitfall worth noting is forgetting the cleanup, which leaks timers and can commit results out of order — both the clearTimeout and the AbortController guard against exactly that. This approach fits any search-as-you-type, autocomplete, or filter box where each keystroke would otherwise trigger expensive work.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

Debounced Search Input in React with a Reusable useDebouncedValue Hook — share card
Link copied