typescript 91 lines · 3 tabs

Debounced search input (React)

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

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

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
3 files · typescript Explain with highlit

This snippet shows the two moving parts behind a responsive search box: a reusable useDebounce hook that delays a rapidly changing value, and a SearchBox component that consumes it while guarding against out-of-order network responses.

Debouncing solves a specific problem. As the user types, the input value changes on every keystroke, but firing a request per keystroke wastes bandwidth and floods the backend. In useDebounce hook, the raw value is copied into debounced state only after a quiet period of delay milliseconds. The effect sets a setTimeout and returns a cleanup function that clears it, so each new keystroke cancels the pending timer before it fires. The consumer therefore only sees the value once typing pauses.

Debouncing alone is not enough, because slow requests can still resolve out of order. Suppose a search for re is slow and a later search for react returns first — without protection, the stale re results would overwrite the fresh ones. SearchBox addresses this with an AbortController. Each time the debounced term changes, the effect stores a fresh controller in controllerRef and passes its signal into fetchResults. The cleanup function calls controller.abort(), which both cancels the in-flight fetch and lets the code ignore its result.

The try/catch distinguishes a real failure from an intentional cancellation: an AbortError name is swallowed silently, while other errors surface through setError. A loading flag drives the UI state, and the guard if (!term) { setResults([]) } short-circuits empty input so no request is made.

The fetchResults helper in api client centralizes the request shape and threads the signal through, keeping the component focused on state rather than transport details. Note that abort rejects the promise, which is why the finally block only clears loading after checking the signal is not already aborted — otherwise a superseded request would prematurely hide the spinner.

The trade-off is latency: a larger delay reduces requests but makes results feel sluggish, so values around 250–400ms are typical. This pattern is the standard way to build autocomplete, typeahead, and filter inputs where every keystroke would otherwise trigger network churn.


Related snips

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
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
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    url: String,
    delay: { type: Number, default: 800 },

Stimulus: autosave draft with Turbo-friendly requests

rails stimulus hotwire
by codesnips 3 tabs
typescript
import React from "react";

type FallbackProps = {
  error: Error;
  reset: () => void;
};

React Error Boundary + error reporting hook

react frontend error-boundary
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Debounced search input (React) — share card
Link copied