Back to Machine Coding
Machine Coding
easy
mid

How would you build a debounced search autocomplete component?

Controlled input → debounced query → fetch with AbortController to cancel stale requests → results dropdown. Must handle: race conditions (out-of-order responses), loading/empty/error states, keyboard navigation (arrows/enter/escape), min query length, and accessibility (combobox ARIA).

5 min read·~25 min to think through

Autocomplete is a deceptively deep component — it's debounce + cancellation + races + keyboard + accessibility all at once.

The implementation

jsx
function Autocomplete() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [status, setStatus] = useState("idle"); // idle|loading|success|error|empty
  const [activeIndex, setActiveIndex] = useState(-1);

  const debouncedQuery = useDebouncedValue(query, 300);

  useEffect(() => {
    if (debouncedQuery.length < 2) { setResults([]); setStatus("idle"); return; }

    const controller = new AbortController();
    setStatus("loading");

    fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`, { signal: controller.signal })
      .then((r) => r.json())
      .then((data) => {
        setResults(data);
        setStatus(data.length ? "success" : "empty");
      })
      .catch((e) => { if (e.name !== "AbortError") setStatus("error"); });

    return () => controller.abort();          // cancel stale request
  }, [debouncedQuery]);

  const onKeyDown = (e) => {
    if (e.key === "ArrowDown") setActiveIndex((i) => Math.min(i + 1, results.length - 1));
    if (e.key === "ArrowUp")   setActiveIndex((i) => Math.max(i - 1, 0));
    if (e.key === "Enter" && activeIndex >= 0) select(results[activeIndex]);
    if (e.key === "Escape")    { setResults([]); setActiveIndex(-1); }
  };
  // ... render input + results list
}

Everything interviewers grade

1. Debounce the query — don't fire a request per keystroke; wait for a ~300ms pause. Debounce the derived value, keep the input itself instant.

2. Cancel stale requests (race conditions)AbortController, aborted in the effect cleanup. This kills the classic bug: you type "react", a slow response for "rea" arrives after the "react" response and overwrites it with wrong results. Debounce alone doesn't fix this — cancellation does.

3. Min query length — don't search on 0–1 chars (noise, expensive).

4. All UI states — loading spinner, results, empty ("no matches"), error (with retry). Not just the happy path.

5. Keyboard navigation — Arrow Up/Down to move the highlight, Enter to select, Escape to close. A search box that's mouse-only is incomplete.

6. Accessibility — it's a combobox: role="combobox", aria-expanded, aria-activedescendant pointing at the highlighted option, role="listbox"/role="option", aria-live for result-count announcements.

7. Other polish — click-outside to close, highlight the matching substring, cache results per query, blur/focus handling, don't show stale results from a previous query.

The framing

"It's a controlled input feeding a debounced query, which drives a fetch with an AbortController, rendering a results dropdown. The four things I'd be sure to nail: debounce the derived query so I'm not firing per keystroke; cancel stale requests in the effect cleanup — because debounce alone doesn't stop an old slow response from overwriting a newer one; handle all states including empty and error, not just results; and full keyboard support — arrows, enter, escape — plus combobox ARIA. The accessibility and the race-condition handling are what separate a real autocomplete from a toy."

Follow-up questions

  • Why doesn't debouncing alone fix out-of-order responses?
  • What ARIA roles and attributes does an autocomplete need?
  • How do you implement keyboard navigation of the results?
  • How would you cache results to avoid refetching the same query?

Common mistakes

  • Firing a request on every keystroke (no debounce).
  • Not cancelling stale requests — out-of-order responses show wrong results.
  • Only handling the success state — no empty/error/loading.
  • Mouse-only — no keyboard navigation.
  • No ARIA — inaccessible to screen readers.

Performance considerations

  • Debounce cuts request volume; cancellation avoids processing discarded responses; caching per query avoids refetching. For large result sets, virtualize the dropdown. Memoize result rows.

Edge cases

  • A slow response for an old query resolving after a newer one.
  • Query cleared while a request is in flight.
  • Zero results — distinct empty state.
  • Very fast typing; user selects with keyboard before results render.
  • Special characters in the query (must encode).

Real-world examples

  • Search bars with live suggestions (Google, e-commerce site search).
  • Address/location autocomplete, @-mention pickers.

Senior engineer discussion

Seniors build debounce + AbortController cancellation together (explaining debounce alone can't fix races), model all UI states, implement full keyboard nav, apply combobox ARIA, and add caching/virtualization for scale.

Related questions