JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionsreact
PrevNext

Learn the concept

Hooks

react
mid
machine-coding

Build an autocomplete/typeahead search component in React that fetches suggestions from an API with debouncing, keyboard navigation, and highlighted matching text.

react
machine-coding
autocomplete
debounce
keyboard-navigation
accessibility
api-integration
interview
Quick Answer

An autocomplete component debounces user input, fetches matching suggestions from an API, renders a dropdown with keyboard navigation (arrow keys + Enter), highlights the matched portion of each suggestion, and handles edge cases like race conditions and empty states.

Detailed Explanation

Core Architecture:

  1. Controlled input with useState for the query string
  2. Debounced API calls — wait 300-500ms after the user stops typing before fetching. Use a custom useDebounce hook or setTimeout with cleanup.
  3. Suggestion list rendered as a <ul> below the input, shown when there are results and the input is focused
  4. Keyboard navigation — track activeIndex with arrow keys, select on Enter, close on Escape
  5. Highlight matching text — split each suggestion into matched and unmatched parts, wrap the match in <mark> or <strong>

Debouncing Strategy: The simplest approach uses useEffect with a setTimeout:

useEffect(() => {
  const timer = setTimeout(() => fetchSuggestions(query), 300);
  return () => clearTimeout(timer);
}, [query]);

This cancels the previous timer whenever the query changes, effectively debouncing.

Race Condition Handling: If the user types "rea" then quickly "reac", both API calls fire. The "rea" response might arrive after "reac", showing stale results. Solutions:

  • Use an AbortController to cancel the previous request
  • Track a request ID and ignore responses from stale requests
  • Use a boolean cancelled flag in the cleanup function

Keyboard Navigation:

  • ArrowDown — increment activeIndex (wrap to 0 at end)
  • ArrowUp — decrement activeIndex (wrap to last at start)
  • Enter — select the item at activeIndex
  • Escape — close the dropdown and clear activeIndex
  • Scroll the active item into view with scrollIntoView()

Accessibility:

  • Input: role="combobox", aria-expanded, aria-activedescendant, aria-controls
  • List: role="listbox" with unique id
  • Items: role="option", aria-selected for active item
  • Announce result count via aria-live region

Performance:

  • Debounce prevents excessive API calls
  • useMemo for the highlight computation if the list is large
  • Consider caching previous results in a useRef map

Code Examples

Autocomplete component with debounce, keyboard nav, and highlightJSX
import { useState, useEffect, useRef, useCallback } from 'react';

function Autocomplete({ fetchSuggestions, onSelect, placeholder = 'Search...' }) {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [loading, setLoading] = useState(false);
  const listRef = useRef(null);
  const abortRef = useRef(null);

  // Debounced fetch
  useEffect(() => {
    if (!query.trim()) {
      setSuggestions([]);
      setIsOpen(false);
      return;
    }

    const timer = setTimeout(async () => {
      // Cancel previous request
      abortRef.current?.abort();
      const controller = new AbortController();
      abortRef.current = controller;

      setLoading(true);
      try {
        const results = await fetchSuggestions(query, controller.signal);
        if (!controller.signal.aborted) {
          setSuggestions(results);
          setIsOpen(results.length > 0);
          setActiveIndex(-1);
        }
      } catch (err) {
        if (err.name !== 'AbortError') console.error(err);
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    }, 300);

    return () => clearTimeout(timer);
  }, [query, fetchSuggestions]);

  const handleKeyDown = useCallback((e) => {
    if (!isOpen) return;
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => (i + 1) % suggestions.length);
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => (i - 1 + suggestions.length) % suggestions.length);
        break;
      case 'Enter':
        if (activeIndex >= 0) {
          e.preventDefault();
          selectItem(suggestions[activeIndex]);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        setActiveIndex(-1);
        break;
    }
  }, [isOpen, activeIndex, suggestions]);

  const selectItem = (item) => {
    setQuery(item.label);
    setIsOpen(false);
    onSelect?.(item);
  };

  // Highlight matching text
  const highlightMatch = (text, query) => {
    const idx = text.toLowerCase().indexOf(query.toLowerCase());
    if (idx === -1) return text;
    return (
      <>
        {text.slice(0, idx)}
        <strong style={{ background: '#fef08a' }}>
          {text.slice(idx, idx + query.length)}
        </strong>
        {text.slice(idx + query.length)}
      </>
    );
  };

  const listId = 'autocomplete-listbox';
  const activeId = activeIndex >= 0 ? `option-${activeIndex}` : undefined;

  return (
    <div style={{ position: 'relative' }}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        onKeyDown={handleKeyDown}
        onFocus={() => suggestions.length > 0 && setIsOpen(true)}
        onBlur={() => setTimeout(() => setIsOpen(false), 150)}
        placeholder={placeholder}
        role="combobox"
        aria-expanded={isOpen}
        aria-controls={listId}
        aria-activedescendant={activeId}
        aria-autocomplete="list"
      />
      {loading && <span aria-live="polite">Loading...</span>}
      {isOpen && (
        <ul id={listId} role="listbox" ref={listRef}
            style={{ position: 'absolute', top: '100%', left: 0, right: 0,
                     border: '1px solid #ddd', background: '#fff',
                     listStyle: 'none', margin: 0, padding: 0, maxHeight: 240,
                     overflowY: 'auto', zIndex: 10 }}>
          {suggestions.map((item, i) => (
            <li key={item.id ?? i} id={`option-${i}`}
                role="option" aria-selected={i === activeIndex}
                onMouseDown={() => selectItem(item)}
                onMouseEnter={() => setActiveIndex(i)}
                style={{
                  padding: '8px 12px', cursor: 'pointer',
                  background: i === activeIndex ? '#f0f0f0' : 'transparent'
                }}>
              {highlightMatch(item.label, query)}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

// Usage with a mock API
const fetchCities = async (query, signal) => {
  const res = await fetch(`/api/cities?q=${query}`, { signal });
  const data = await res.json();
  return data.map(c => ({ id: c.id, label: c.name }));
};

function SearchPage() {
  return <Autocomplete fetchSuggestions={fetchCities}
                       onSelect={(city) => console.log('Selected:', city)} />;
}

Real-World Applications

Use Cases

Restaurant Search

Food delivery apps show restaurant and dish suggestions as the user types, with results debounced to avoid hammering the search API.

Address Autocomplete

E-commerce checkout forms use typeahead powered by Google Places or Mapbox APIs to speed up address entry.

Command Palette

Developer tools like VS Code, Figma, and Linear use autocomplete-style command palettes for quick navigation.

Mini Projects

GitHub User Search

intermediate

Build an autocomplete that searches GitHub users via the public API, shows avatars in results, and navigates to the profile on selection.

Multi-select Tags Input

advanced

Extend autocomplete into a tags input where selected items appear as removable chips and the dropdown filters out already-selected items.

Industry Examples

Swiggy

Autocomplete/autosuggestion with API integration is a confirmed Swiggy machine coding round question. Their search bar handles restaurant names, cuisines, and dish searches.

Google

Google Search's autocomplete is one of the most well-known typeahead implementations, suggesting queries as users type with sub-100ms latency.

Resources

WAI-ARIA Combobox Pattern

docs

MDN - AbortController

docs

React Docs - Synchronizing with Effects

docs

Related Questions

Explain the useState and useEffect hooks and their common patterns.

mid
hooks

What are custom hooks and how do you create them?

mid
hooks
Previous
Build an OTP (One-Time Password) input component in React that auto-advances focus between fields, supports paste, and handles backspace navigation.
Next
Build an infinite scroll component in React using Intersection Observer that loads more items as the user scrolls, with loading states and race condition handling.
PrevNext