javascript 87 lines · 3 tabs

Cancel Stale Autocomplete Requests with AbortController in a React Hook

Shared by codesnips Jul 2026
3 tabs
const BASE_URL = "/api/search";

export async function searchProducts(query, { signal } = {}) {
  const params = new URLSearchParams({ q: query, limit: "10" });
  const res = await fetch(`${BASE_URL}?${params}`, {
    signal,
    headers: { Accept: "application/json" },
  });

  if (!res.ok) {
    throw new Error(`Search failed with status ${res.status}`);
  }

  const body = await res.json();
  return body.items;
}
3 files · javascript Explain with highlit

This snippet shows how to build a self-cancelling search hook so that fast typing never renders results from a stale request. The core problem is a race condition: when the query changes rapidly, several fetch calls are in flight at once, and network latency means they can resolve out of order. Without cancellation, an older request that finishes last would overwrite the results for the newest query, producing flickering or plain wrong suggestions.

The useSearch hook solves this with AbortController. Each effect run creates a fresh controller and passes controller.signal into fetch. The effect's cleanup function calls controller.abort(), which React invokes both when query changes and when the component unmounts. Because a new effect run supersedes the previous one, the in-flight request tied to the old query is aborted the moment a newer keystroke arrives, so only the latest request can ever call setResults.

A debounce timer sits in front of the fetch so that transient keystrokes do not each trigger a request; the setTimeout is cleared in the same cleanup path. When fetch is aborted it rejects with an AbortError, which the catch explicitly ignores via the err.name check — aborting is expected control flow, not a real failure. This keeps the error state clean and avoids setting state after cleanup.

The searchApi client centralizes the request so components never touch fetch directly. It accepts the shared signal, builds the URL with URLSearchParams, and throws on non-ok responses so the hook's catch can surface real errors. Notice it does not swallow AbortError; that concern belongs to the caller.

SearchBox component wires everything together, feeding the controlled input value into the hook and rendering loading, error, and results states. The trade-off is that aborted requests still consume some server work, but the client stays correct and responsive. This pattern is the standard fix whenever an input drives async work — search, typeahead, filters — and is far more robust than a manual "is this still the latest request" flag.


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
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
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

Share this code

Here's the card — post it anywhere.

Cancel Stale Autocomplete Requests with AbortController in a React Hook — share card
Link copied