Design an Autocomplete Component
Concepts: Debouncing, Keyboard Navigation, Request Cancellation (AbortController), Caching, Text Highlighting, Focus Management, ARIA Attributes, Race Condition Handling
Requirements
| Requirement | Description |
|---|---|
| Show suggestions | Display matching results as user types |
| Keyboard navigation | Navigate suggestions with arrow keys |
| Recent searches | Display previously searched terms |
| Highlight matching text | Emphasize query match in suggestions |
| Debounce input | Limit 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 Strategy | Description |
|---|---|
| TTL-based | Expire entries after fixed duration |
| LRU | Evict least recently used entries when limit reached |
| Size-limited | Cap 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 Attribute | Purpose |
|---|---|
| role="combobox" | Identifies input as autocomplete |
| aria-autocomplete="list" | Indicates suggestions are provided |
| aria-expanded | Indicates dropdown visibility |
| aria-activedescendant | Points to currently focused option |
| role="listbox" | Identifies suggestion container |
| role="option" | Identifies individual suggestions |
| aria-selected | Indicates 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
| Decision | Options | Considerations |
|---|---|---|
| Debounce delay | 100-500ms | Lower values increase responsiveness; higher values reduce API load |
| Cache size | Memory limit | Larger cache improves hit rate; consumes more memory |
| Minimum query length | 1-3 characters | Shorter values show more suggestions; increase API load |
| Result count | 5-20 items | More results provide options; may overwhelm users |
| Cache TTL | Minutes to hours | Longer TTL reduces API calls; may show stale data |