Skip to main content

Design an Autocomplete Component

Concepts: Debouncing, Keyboard Navigation, Request Cancellation (AbortController), Caching, Text Highlighting, Focus Management, ARIA Attributes, Race Condition Handling

Requirements

RequirementDescription
Show suggestionsDisplay matching results as user types
Keyboard navigationNavigate suggestions with arrow keys
Recent searchesDisplay previously searched terms
Highlight matching textEmphasize query match in suggestions
Debounce inputLimit API calls during rapid typing

Component Architecture

The Autocomplete component manages the following state:

  • query: The current input value
  • suggestions: Array of matching results from the API
  • isOpen: Whether the suggestion dropdown is visible
  • activeIndex: Index of the currently highlighted suggestion for keyboard navigation

Input handling: When the user types, update the query state, open the dropdown, and call a debounced function to fetch suggestions. Skip API calls for queries shorter than a minimum length (typically 2 characters).

Keyboard navigation: Handle keydown events on the input:

  • ArrowDown: Move activeIndex down, clamped to suggestions length
  • ArrowUp: Move activeIndex up, clamped to -1 (no selection)
  • Enter: Select the highlighted suggestion if one is active
  • Escape: Close the dropdown

Rendering: The input element receives change and keydown handlers. When the dropdown is open and suggestions exist, render a SuggestionList component that displays results with highlighting for the matching query text.

Text Highlighting

The HighlightedText component marks matching portions of suggestion text.

Implementation approach: Split the suggestion text using the query as a delimiter (case-insensitive). This produces an array of alternating non-matching and matching segments. When rendering, wrap matching segments in a <mark> element for visual emphasis while leaving non-matching segments as plain text.

Response Caching

Caching reduces redundant API calls for repeated queries.

Implementation approach: Use a Map to store query results with timestamps. Before making an API call, check if the query exists in the cache and whether the cached entry is still valid based on a TTL (time-to-live) value, such as 5 minutes. If valid, return the cached data. If not, fetch from the API, store the result with the current timestamp, and return the data.

Cache StrategyDescription
TTL-basedExpire entries after fixed duration
LRUEvict least recently used entries when limit reached
Size-limitedCap total cache size in memory

Recent Searches

Store recent searches in localStorage for persistence across sessions.

Implementation approach: Create a custom hook that initializes state from localStorage on mount. When adding a new search, add it to the front of the array, remove duplicates, limit to a maximum count (such as 10 items), update state, and persist back to localStorage. The hook returns the recent searches array and an addSearch function.

Accessibility

ARIA attributes enable screen reader support for the autocomplete component.

Input element: Set role="combobox" to identify it as an autocomplete. Use aria-autocomplete="list" to indicate suggestions are provided. Set aria-expanded based on dropdown visibility. Point aria-activedescendant to the ID of the currently highlighted suggestion (when applicable). Link to the suggestion list with aria-controls.

Suggestion list: Use role="listbox" on the container. Each suggestion receives role="option", a unique ID, and aria-selected to indicate whether it is currently highlighted.

ARIA AttributePurpose
role="combobox"Identifies input as autocomplete
aria-autocomplete="list"Indicates suggestions are provided
aria-expandedIndicates dropdown visibility
aria-activedescendantPoints to currently focused option
role="listbox"Identifies suggestion container
role="option"Identifies individual suggestions
aria-selectedIndicates current selection state

Request Cancellation

AbortController cancels pending requests when a new query is entered.

Implementation approach: Create a custom hook that maintains a ref to the current AbortController. Before each fetch, check if a previous controller exists and call abort() on it. Create a new AbortController and pass its signal to the fetch call. This ensures that when the user types quickly, previous in-flight requests are cancelled, preventing race conditions where an older response arrives after a newer one.

Design Trade-offs

DecisionOptionsConsiderations
Debounce delay100-500msLower values increase responsiveness; higher values reduce API load
Cache sizeMemory limitLarger cache improves hit rate; consumes more memory
Minimum query length1-3 charactersShorter values show more suggestions; increase API load
Result count5-20 itemsMore results provide options; may overwhelm users
Cache TTLMinutes to hoursLonger TTL reduces API calls; may show stale data