Summary
Autocomplete fetches suggestions as the user types. The implementation surface covers debouncing keystrokes to avoid flooding the server, cancelling in-flight requests with `AbortController` to prevent out-of-order renders, per-query caching with stale-while-revalidate, and the ARIA combobox pattern for accessible keyboard navigation.
Jump to the interview angleAn autocomplete widget binds an <input> to a suggestion list that updates on every keystroke. Three problems dominate the implementation:
Debounce delays the fetch until the user pauses (typically 200–300 ms), collapsing rapid keystrokes into one request per query.
Request cancellation ensures that if the user types faster than the server responds, only the result for the latest query is applied. AbortController + signal wires this into fetch.
Out-of-order responses — without cancellation, a slow reply for "ab" can resolve after a fast reply for "abc", replacing newer results with older ones. Aborting the previous request prevents this entirely.
Request lifecycle per keystroke
- 1
Keystroke fires
The input's
onChangehandler resets a debounce timer. If a timer was already pending, it's cleared. No network call happens yet. - 2
Debounce timer fires (200–300 ms)
The timer callback checks the query length against a minimum character threshold (typically 2). Shorter queries are skipped to avoid low-signal results.
- 3
Abort previous request
If a previous
AbortControllerexists, callcontroller.abort(). This cancels the in-flightfetchand prevents its.then()chain from running. - 4
Create new controller and fetch
Instantiate a new
AbortController, passsignaltofetch, then set loading state. The signal is stored in auseRefso the next keystroke can reach it. - 5
Render results or state
On success, populate the suggestion list from cache or the fresh response. On abort, do nothing. On network error, show the error state. On empty results, show the empty state.
Debounced fetch with AbortController and per-query cache
A single useRef holds the active controller. Each fetch checks the cache first; on miss it aborts the previous request and fires a new one.
import { useState, useEffect, useRef } from "react";
type Status = "idle" | "loading" | "success" | "error";
interface TypeaheadState {
suggestions: string[];
status: Status;
}
const cache = new Map<string, string[]>();
export function useTypeahead(query: string, minChars = 2, debounceMs = 250) {
const [state, setState] = useState<TypeaheadState>({
suggestions: [],
status: "idle",
});
const controllerRef = useRef<AbortController | null>(null);
useEffect(() => {
if (query.length < minChars) {
setState({ suggestions: [], status: "idle" });
return;
}
// Return cached result immediately (stale-while-revalidate omitted for brevity)
if (cache.has(query)) {
setState({ suggestions: cache.get(query)!, status: "success" });
return;
}
const timer = setTimeout(async () => {
// Abort any in-flight request before starting a new one
controllerRef.current?.abort();
const controller = new AbortController();
controllerRef.current = controller;
setState((s) => ({ ...s, status: "loading" }));
try {
const res = await fetch(
`/api/suggestions?q=${encodeURIComponent(query)}`,
{ signal: controller.signal }
);
if (!res.ok) throw new Error(res.statusText);
const data: string[] = await res.json();
cache.set(query, data);
setState({ suggestions: data, status: "success" });
} catch (err) {
if ((err as Error).name === "AbortError") return; // expected; ignore
setState({ suggestions: [], status: "error" });
}
}, debounceMs);
return () => {
clearTimeout(timer);
controllerRef.current?.abort();
};
}, [query, minChars, debounceMs]);
return state;
}The cleanup function both clears the pending timer and aborts the active request. Checking err.name === 'AbortError' prevents treating a deliberate abort as an error state.
ARIA combobox pattern
Set role="combobox" on the <input>, aria-expanded to reflect whether the list is open, and aria-activedescendant to the id of the focused option. The list element gets role="listbox"; each option gets role="option". Keyboard nav: Arrow keys move aria-activedescendant, Enter selects, Escape closes and returns focus to the input.
Interview angle
Interviewers probe whether you know why AbortController matters — not just that it exists. Explain out-of-order response prevention, then cover the ARIA combobox contract and keyboard nav. Mention cache strategy (Map or SWR-style stale-while-revalidate) and min-chars threshold.
Soundbite: "Debounce batches keystrokes; AbortController prevents stale responses from overwriting fresh ones."
Key terms
- AbortController
- Browser API that produces a `signal` for cancelling fetch requests mid-flight.
- debounce
- Delay firing a function until input pauses; collapses rapid events into one call.
- ARIA combobox
- `role="combobox"` on the input plus `role="listbox"` on the list; enables screen-reader announcement of suggestions.
- stale-while-revalidate
- Return cached data immediately while fetching an update in the background.
- aria-activedescendant
- Points the input's accessible focus to the currently highlighted option id in the listbox.