javascript 98 lines · 3 tabs

Debounced Search Box With Live Autocomplete in React

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

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

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

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

  return debounced;
}
3 files · javascript Explain with highlit

This snippet shows how a search box can stay responsive while querying a remote API only after the user pauses typing. The core idea is debouncing: instead of firing a request on every keystroke, the input value is buffered and a request is issued once the value has been stable for a short interval. This cuts network chatter dramatically and avoids hammering the backend on fast typists, at the cost of a small, deliberate latency before suggestions appear.

The useDebounce hook implements the primitive in isolation. It stores the latest value in state and schedules a setTimeout that copies the incoming value into debounced after delay milliseconds. The cleanup function returned from useEffect clears the pending timer whenever value changes, so rapid edits keep resetting the clock and only the final keystroke survives. This is the classic trailing-edge debounce, and keeping it as a generic hook means it can wrap any fast-changing value, not just search text.

The useAutocomplete hook layers data fetching on top. It debounces the raw query, then reacts to changes in the debounced term. Two subtleties matter here. First, an AbortController is created per request and torn down on cleanup, so if a newer query supersedes an in-flight one, the stale response is cancelled rather than racing to overwrite fresher results — this prevents the out-of-order response bug that plagues naive typeaheads. Second, empty or whitespace-only queries short-circuit to an empty list without touching the network. AbortError is swallowed intentionally because a cancelled request is expected, not a failure.

The SearchBox component wires the hook to the DOM. It is a controlled input driven by query state, and it renders loading, error, and the results list conditionally. Each suggestion is keyed by a stable id, and selecting one pushes its label back into the input. Because all timing and race logic lives in the hooks, the component stays declarative and easy to reason about.

A developer reaches for this pattern whenever live search, address lookup, or @-mention pickers are needed. The main pitfalls it addresses are excessive requests, flickering results, and stale responses overwriting new ones — all handled by combining debounce with request cancellation.


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
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"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 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 Box With Live Autocomplete in React — share card
Link copied