typescript 94 lines · 3 tabs

Debounced Search Box With AbortController to Cancel Stale Fetches in React

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

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

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

  return debounced;
}
3 files · typescript Explain with highlit

This snippet shows how a live search box avoids two classic problems at once: hammering the backend on every keystroke, and rendering results from a request that has already been superseded. The solution combines a debounce hook with AbortController and a small amount of state discipline.

In useDebouncedValue hook, the raw input value is delayed by a configurable delay. Each change schedules a setTimeout and the effect's cleanup clears the previous timer, so only the last value within the quiet window survives. This collapses a burst of keystrokes into a single downstream update, which is what actually triggers a fetch. The hook is generic over T so it works for strings or any serializable filter object.

useSearch hook reacts to that debounced term. On every change it creates a fresh AbortController and passes its signal into fetch. The effect's cleanup calls controller.abort(), which cancels the in-flight request whenever the term changes again or the component unmounts. This is the key to eliminating race conditions: without it, a slow early request could resolve after a fast later one and overwrite the correct results. An AbortError is expected and swallowed, while genuine failures are surfaced through error. The status field distinguishes idle, loading, success, and error so the UI can render meaningfully rather than guessing from results.length.

Note the guard for an empty term, which short-circuits to idle instead of firing a pointless request. Because fetch rejects with a DOMException named AbortError on cancellation, the code checks err.name rather than swallowing everything, so real network problems still bubble up.

SearchBox component wires it together: a controlled input feeds useDebouncedValue, whose output feeds useSearch. The component stays dumb, simply mapping status to loading, empty, and result views. The trade-off is a deliberate latency of delay milliseconds before results appear, which is almost always worth it versus the cost and flicker of unthrottled requests. This pattern generalizes to autocomplete, typeahead filters, and any input-driven remote query where staleness and request volume both matter.


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
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
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 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

Share this code

Here's the card — post it anywhere.

Debounced Search Box With AbortController to Cancel Stale Fetches in React — share card
Link copied