# Autocomplete / Typeahead
> Debounced input, AbortController cancellation, and ARIA combobox.
Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/autocomplete-typeahead
Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end.
## 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.
## Definition
An autocomplete widget binds an `` 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 `onChange` handler 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 `AbortController` exists, call `controller.abort()`. This cancels the in-flight `fetch` and prevents its `.then()` chain from running.
### 4. Create new controller and fetch
Instantiate a new `AbortController`, pass `signal` to `fetch`, then set loading state. The signal is stored in a `useRef` so 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.
**useTypeahead — debounce, abort, cache**
```tsx
import { useState, useEffect, useRef } from "react";
type Status = "idle" | "loading" | "success" | "error";
interface TypeaheadState {
suggestions: string[];
status: Status;
}
const cache = new Map();
export function useTypeahead(query: string, minChars = 2, debounceMs = 250) {
const [state, setState] = useState({
suggestions: [],
status: "idle",
});
const controllerRef = useRef(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.
## Note: ARIA combobox pattern
Set `role="combobox"` on the ``, `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.
## 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.
## Related topics
- [Client State Management](https://fearchitect.com/topics/client-state-management.md): Decide where UI state lives; pick the right tool for the scope.
- [Server State & Data Fetching](https://fearchitect.com/topics/server-state-data-fetching.md): Async, shared, remote-owned data that requires a dedicated cache layer.
- [Accessibility (a11y)](https://fearchitect.com/topics/accessibility.md): WCAG 2.2 AA: semantic HTML, keyboard nav, and ARIA done right.
- [Data Table / Data Grid](https://fearchitect.com/topics/data-table.md): Headless table logic separate from rendering, with server-side ops for large datasets.
- [File Upload](https://fearchitect.com/topics/file-upload.md): Presigned URLs, chunked uploads, progress UI, and retry.
- [Infinite Scroll & Feeds](https://fearchitect.com/topics/infinite-scroll-feed.md): Cursor-paginated feed with a bounded DOM and accessible fallback.
## Further reading
- [ARIA Authoring Practices Guide — Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/)
- [MDN — AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
- [MDN — fetch: signal option](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#signal)
- [WHATWG — Aborting ongoing activities](https://dom.spec.whatwg.org/#aborting-ongoing-activities)