Back to Machine Coding
Machine Coding
medium
mid

How would you architect a search bar with debounced input and async suggestions?

Debounce input (250–300ms), cancel stale requests with AbortController, race-condition guard via request-id or sequence number, cache results by query, render a listbox combobox with ARIA, keyboard nav (↑↓ Enter Esc), and highlight match. Use React Query for caching + request dedup. Test for double-fire on backspace and quick paste.

5 min read·~30 min to think through

Search-as-you-type looks simple. It's a magnet for bugs: stale responses, race conditions, leaky requests, and broken keyboard nav.

Anatomy

ts
[ input ]  ─ debounce 250ms ─→ fetch(/search?q=…) ─→ Suggestions

keydown (↑↓ Enter Esc) ─→ navigate listbox

Debounce

tsx
const debounced = useDebouncedValue(q, 250);
const { data } = useQuery(["search", debounced], ({ signal }) => fetch(...), { enabled: !!debounced });

250–300ms feels responsive without over-firing. For very fast typers, 200ms.

Cancellation

ts
async function fetchSuggest(q, { signal }) {
  const res = await fetch(`/search?q=${encodeURIComponent(q)}`, { signal });
  return res.json();
}

React Query passes signal automatically — old requests abort when the query key changes. Without that, you risk: type "ja" then "javascript"; "ja" response arrives last and overwrites "javascript" results — the classic stale-response race.

Manual race guard (if not using React Query)

ts
let seq = 0;
async function search(q) {
  const id = ++seq;
  const data = await fetch(...);
  if (id !== seq) return;   // a newer request superseded us
  setResults(data);
}

Caching

Last 5–10 queries cached. React Query handles this; on its own use a Map with LRU eviction.

Keyboard + ARIA combobox

html
<input
  role="combobox"
  aria-expanded="true"
  aria-controls="list"
  aria-activedescendant="opt-2"
/>
<ul role="listbox" id="list">
  <li role="option" id="opt-1" aria-selected="false">…</li>
  <li role="option" id="opt-2" aria-selected="true">…</li>
</ul>
  • ↓/↑ move active option (update aria-activedescendant).
  • Enter selects.
  • Esc clears + closes.
  • Home/End jump to first/last.
  • Tab closes and moves focus naturally.

Adopt Radix Combobox / Downshift / cmdk — the WAI-ARIA combobox spec is tricky.

Highlighting

Highlight the matched substring in each suggestion. Compute once on render; don't store HTML in state.

Loading + empty states

  • Pending: skeleton or "Searching…".
  • Empty after typed: "No results for X."
  • Error: "Couldn't load. Retry."

Edge cases

  • Empty query — don't fetch.
  • Rapid backspace — debounce should collapse; cancellation handles in-flight.
  • Paste a long string — same debounce, but no double-fire.
  • Trailing whitespace — trim before fetching.
  • Min length (e.g. 2 chars) before firing.
  • Re-focus + same query — read from cache, don't refetch.
  • Mobile keyboardinputMode / enterkeyhint="search".

Server-side concerns

  • Rate limit per user.
  • Index for prefix matching (Postgres trigram, Elastic, Algolia).
  • Return relevance + result count.

Interview framing

"Debounce input ~250ms. On each fired query, cancel the previous via AbortController — that single thing eliminates the stale-response bug. React Query handles cancellation, caching, and dedup for free. Combobox UI with proper ARIA — role="combobox" on input, role="listbox" and role="option" on the list, aria-activedescendant updated on ↑↓. Handle empty, loading, error, and 'no results' explicitly. Don't fire on empty query. The bugs I'd test for: stale response overwriting newer, keyboard nav, paste burst, rapid backspace."

Follow-up questions

  • Walk through the stale-response race and the fix.
  • How does AbortController integrate with fetch?
  • Why useDeferredValue might help here?

Common mistakes

  • No cancellation → stale responses overwrite.
  • No debounce → 10 requests per word.
  • Selecting with onClick only (no keyboard).
  • Missing aria-activedescendant.

Performance considerations

  • Debounce + cancel + cache cut request count 10×. Memoize suggestion item component.

Edge cases

  • User pastes 2KB.
  • Network drops mid-request.
  • Same query refocus — should hit cache.
  • i18n input (composition events for IME).

Real-world examples

  • Algolia DocSearch, Linear's cmd-K, GitHub search, VSCode quick-open.

Senior engineer discussion

Seniors anchor on cancellation + race guards first, ARIA combobox second, perf third. They use battle-tested combobox libraries instead of rolling their own ARIA.

Related questions