Skip to content

Components

React Search Autocomplete — Debounce, Abort, and the Out-of-Order Result Bug

The Race Condition Every Autocomplete Ships — and the AbortController That Kills It
Written by Aiden Wood Founder and Lead Design Engineer
🔎 ⏱️ 🎹

Autocomplete looks like a solved problem until you watch the network tab: a request per keystroke, and — the classic — results for "vel" arriving after results for "velocity" and overwriting them. The user typed more, the UI showed less. That's not a style bug; it's a race condition shipping to production on most search boxes.

Velocity X's search-input-suggestions handles the three failure modes in one file: request spam, out-of-order responses, and the keyboard model.

The Money Pattern

Debounce controls when you fetch; AbortController guarantees stale responses can never land:

useEffect(() => {
  if (query.length < 2) { setResults([]); return; }

  const ctrl = new AbortController();
  const t = setTimeout(async () => {
    try {
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`,
        { signal: ctrl.signal });
      setResults(await res.json());
    } catch (e) {
      if (e.name !== 'AbortError') setResults([]);
    }
  }, 150);

  return () => { clearTimeout(t); ctrl.abort(); };
}, [query]);

The cleanup function is the whole fix: every new keystroke cancels both the pending timer and any in-flight request. Out-of-order results become impossible — not unlikely, impossible — because a superseded request is dead before its response arrives. No sequence counters, no "latest query" refs.

The Catch

The dropdown is where accessibility goes to die. The pattern has a name — combobox — and a contract: role="combobox" with aria-expanded on the input, role="listbox" on the list, arrow keys moving an aria-activedescendant highlight while focus stays in the input so typing never stops working. Moving real focus into the list is the most common way to break it.

And 150ms is a starting point, not a law: local/in-memory search wants no debounce at all, a rate-limited API wants 300ms. Debounce is a cost dial, so measure the API you're actually calling.

The Verdict

Autocomplete is a cancellation problem wearing a UI. Debounce the timer, abort the fetch, keep focus in the input — three decisions, all in the cleanup function. The finished combobox lives in Velocity X's Forms category, with standard-searchable-select-menu next door when the job is picking from a known list instead of searching an index.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.