Designing an Autocomplete System
Problem Statement
Design a reusable and scalable autocomplete component that provides real-time search suggestions as users type. The component should handle various data sources, support customization, and ensure optimal performance and accessibility.
Key Requirements:
- Real-time suggestions as users type
- Multiple data sources (API, local data, or both)
- Keyboard navigation and mouse interactions
- Performance optimization with debouncing and caching
- Accessibility compliance (WCAG 2.1)
- Customizable styling and behavior
- Error handling and loading states
Clarifying Questions
Before diving into the design, consider the following:
Functional Requirements
- Data Source: Will suggestions come from a remote API, local static data, or both?
- Result Types: Are suggestions limited to text, or do they include images, links, or other formats?
- User Interaction: Should the component support keyboard navigation, mouse interactions, or both?
- Performance: What are the expectations regarding response time and scalability?
- Accessibility: What accessibility standards need to be met?
- Internationalization: Is support for multiple languages required?
- Offline Support: Should the component function without an internet connection?
Technical Constraints
- Browser Support: What browsers and versions need to be supported?
- Bundle Size: Are there size constraints for the component?
- Framework: Is this for React, Vue, vanilla JS, or framework-agnostic?
- Mobile Support: Does it need to work on touch devices?
High-Level Architecture
1. Input Handler
- Captures user input with proper event handling
- Implements debouncing to limit the frequency of API calls
- Triggers suggestion fetching based on input changes
- Handles input validation and sanitization
2. Suggestion Fetcher
- Retrieves suggestions from the specified data source
- Supports both synchronous (local data) and asynchronous (API) fetching
- Handles errors and loading states gracefully
- Implements retry logic for failed requests
3. Cache Layer
- Stores recent queries and their results to reduce redundant fetches
- Implements strategies like Least Recently Used (LRU) eviction
- Enhances performance, especially for repeated queries
- Supports cache invalidation and expiration
4. Suggestion Renderer
- Displays the list of suggestions with proper virtualization
- Highlights matching substrings for better UX
- Supports customization for different result types (e.g., text, images)
- Handles empty states and loading indicators
5. Interaction Handler
- Manages keyboard and mouse interactions
- Allows navigation through suggestions using arrow keys
- Handles selection via Enter key or mouse click
- Manages focus states and accessibility
Data Handling Strategies
Debouncing
Implement a debounce mechanism to delay the fetch operation until the user stops typing for a specified duration (typically 300-500ms). This reduces unnecessary API calls and improves performance.
const useDebounce = (value: string, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);Caching
Use a caching strategy to store previous query results. A simple Map or LRU cache can be used, where the query string is the key, and the suggestions are the value.
class AutocompleteCache {
private cache = new Map<string, Suggestion[]>();
private maxSize = 100;
get(query: string): Suggestion[] | undefined {
return this.cache.get(query);
}
set(query: string, suggestions: Suggestion[]): void {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(query, suggestions);
}
}Fuzzy Search
For local data sources, implement fuzzy search algorithms (e.g., Levenshtein distance, Fuse.js) to provide suggestions even when the user's input has minor typos or variations.
Customization and Flexibility
Styling Options
- Theme Support: Allow customization of colors, fonts, and spacing
- Responsive Design: Adapt to different screen sizes
- Dark Mode: Support for light and dark themes
- Custom CSS Classes: Allow external styling
Configuration Options
- Debounce Delay: Configurable delay for API calls
- Minimum Input Length: Minimum characters before showing suggestions
- Maximum Suggestions: Limit the number of displayed suggestions
- Highlighting: Customize how matching text is highlighted
Result Templates
Support custom templates for rendering different types of suggestions:
- Simple text suggestions
- Suggestions with images and descriptions
- Structured data with multiple fields
Accessibility and Internationalization
Accessibility (WCAG 2.1 Compliance)
- Keyboard Navigation: Full keyboard support with proper focus management
- ARIA Attributes: Use appropriate ARIA roles and attributes
- Screen Reader Support: Ensure compatibility with assistive technologies
- Focus Management: Proper focus handling during interactions
// Example ARIA attributes
<div
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-controls="suggestions-list"
aria-activedescendant={activeSuggestionId}
>
<input
aria-autocomplete="list"
aria-controls="suggestions-list"
/>
<ul
id="suggestions-list"
role="listbox"
>
{suggestions.map((suggestion, index) => (
<li
key={suggestion.id}
role="option"
aria-selected={index === activeIndex}
id={`suggestion-${index}`}
>
{suggestion.text}
</li>
))}
</ul>
</div>Internationalization
- Multiple Languages: Support for different languages and locales
- RTL Support: Right-to-left text direction support
- Localized Placeholders: Customizable placeholder text
- Number Formatting: Proper number and date formatting
Performance and Scalability
Optimization Techniques
- Virtualization: For large suggestion lists, use virtual scrolling
- Lazy Loading: Load suggestions as needed
- Pagination: For large datasets, implement pagination or infinite scrolling
- Throttling: Limit the rate of API calls to prevent server overload
Memory Management
- Cache Size Limits: Prevent memory leaks with cache size limits
- Cleanup: Proper cleanup of event listeners and timers
- Garbage Collection: Optimize for garbage collection
Complete Implementation
import React, { useState, useEffect, useRef, useCallback } from "react";
interface Suggestion {
id: string;
text: string;
value: any;
}
interface AutocompleteProps {
data?: Suggestion[];
onSearch?: (query: string) => Promise<Suggestion[]>;
debounceDelay?: number;
minLength?: number;
maxSuggestions?: number;
placeholder?: string;
onSelect?: (suggestion: Suggestion) => void;
renderSuggestion?: (suggestion: Suggestion, isHighlighted: boolean) => React.ReactNode;
}
const LOCAL_DATA: Suggestion[] = [
{ id: "1", text: "Apple", value: "apple" },
{ id: "2", text: "Banana", value: "banana" },
{ id: "3", text: "Blueberry", value: "blueberry" },
{ id: "4", text: "Cherry", value: "cherry" },
{ id: "5", text: "Date", value: "date" },
{ id: "6", text: "Durian", value: "durian" },
{ id: "7", text: "Grape", value: "grape" },
{ id: "8", text: "Kiwi", value: "kiwi" },
{ id: "9", text: "Lemon", value: "lemon" },
{ id: "10", text: "Mango", value: "mango" },
];
// Simple substring match for demo (replace with fuzzy search if needed)
function filterSuggestions(query: string, data: Suggestion[]): Suggestion[] {
if (!query) return [];
const lower = query.toLowerCase();
return data.filter((item) =>
item.text.toLowerCase().includes(lower)
);
}
// Debounce hook
function useDebounce(value: string, delay: number): string {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
export default function Autocomplete({
data = LOCAL_DATA,
onSearch,
debounceDelay = 300,
minLength = 1,
maxSuggestions = 10,
placeholder = "Search...",
onSelect,
renderSuggestion,
}: AutocompleteProps) {
const [input, setInput] = useState("");
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [highlightIndex, setHighlightIndex] = useState(-1);
const [showSuggestions, setShowSuggestions] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Cache: Map query -> suggestions
const cacheRef = useRef(new Map<string, Suggestion[]>());
const debouncedInput = useDebounce(input, debounceDelay);
// Refs for keyboard navigation & accessibility
const inputRef = useRef<HTMLInputElement>(null);
const suggestionsRef = useRef<HTMLUListElement>(null);
// Fetch suggestions (local + cache)
useEffect(() => {
const query = debouncedInput.trim();
if (query.length < minLength) {
setSuggestions([]);
setShowSuggestions(false);
return;
}
// Check cache first
if (cacheRef.current.has(query)) {
setSuggestions(cacheRef.current.get(query)!);
setShowSuggestions(true);
return;
}
const fetchSuggestions = async () => {
setIsLoading(true);
setError(null);
try {
let results: Suggestion[];
if (onSearch) {
// Use provided search function
results = await onSearch(query);
} else {
// Use local data with filtering
results = filterSuggestions(query, data);
}
// Limit results
results = results.slice(0, maxSuggestions);
// Cache results
cacheRef.current.set(query, results);
setSuggestions(results);
setShowSuggestions(true);
} catch (err) {
setError('Failed to fetch suggestions');
setSuggestions([]);
} finally {
setIsLoading(false);
}
};
fetchSuggestions();
}, [debouncedInput, data, maxSuggestions, minLength, onSearch]);
// Handle keyboard events
const onKeyDown = useCallback((e: React.KeyboardEvent) => {
if (!showSuggestions) return;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setHighlightIndex((prev) =>
prev < suggestions.length - 1 ? prev + 1 : 0
);
break;
case "ArrowUp":
e.preventDefault();
setHighlightIndex((prev) =>
prev > 0 ? prev - 1 : suggestions.length - 1
);
break;
case "Enter":
e.preventDefault();
if (highlightIndex >= 0 && highlightIndex < suggestions.length) {
selectSuggestion(suggestions[highlightIndex]);
}
break;
case "Escape":
setShowSuggestions(false);
setHighlightIndex(-1);
break;
}
}, [showSuggestions, suggestions, highlightIndex]);
// Select suggestion
const selectSuggestion = useCallback((suggestion: Suggestion) => {
setInput(suggestion.text);
setShowSuggestions(false);
setHighlightIndex(-1);
if (onSelect) onSelect(suggestion);
}, [onSelect]);
// Handle click outside to close suggestions
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
suggestionsRef.current &&
!suggestionsRef.current.contains(event.target as Node) &&
inputRef.current &&
!inputRef.current.contains(event.target as Node)
) {
setShowSuggestions(false);
setHighlightIndex(-1);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Highlight matching substring
const highlightText = (text: string, query: string) => {
if (!query) return text;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const startIndex = lowerText.indexOf(lowerQuery);
if (startIndex === -1) return text;
const beforeStr = text.substring(0, startIndex);
const matchStr = text.substring(startIndex, startIndex + query.length);
const afterStr = text.substring(startIndex + query.length);
return (
<>
{beforeStr}
<strong>{matchStr}</strong>
{afterStr}
</>
);
};
return (
<div className="autocomplete-container" style={{ maxWidth: 300, margin: "auto", position: "relative" }}>
<input
type="text"
ref={inputRef}
aria-autocomplete="list"
aria-controls="autocomplete-list"
aria-activedescendant={
highlightIndex >= 0 ? `suggestion-${highlightIndex}` : undefined
}
aria-expanded={showSuggestions}
aria-haspopup="listbox"
role="combobox"
placeholder={placeholder}
value={input}
onChange={(e) => {
setInput(e.target.value);
setHighlightIndex(-1);
}}
onKeyDown={onKeyDown}
autoComplete="off"
style={{
width: "100%",
padding: "8px 12px",
fontSize: 16,
border: "1px solid #ccc",
borderRadius: 4,
boxSizing: "border-box",
}}
/>
{showSuggestions && (
<ul
id="autocomplete-list"
role="listbox"
ref={suggestionsRef}
style={{
position: "absolute",
top: "100%",
left: 0,
right: 0,
background: "white",
border: "1px solid #ccc",
borderTop: "none",
margin: 0,
padding: 0,
listStyle: "none",
maxHeight: 200,
overflowY: "auto",
zIndex: 1000,
borderRadius: "0 0 4px 4px",
}}
>
{isLoading && (
<li style={{ padding: "8px 12px", color: "#666" }}>
Loading...
</li>
)}
{error && (
<li style={{ padding: "8px 12px", color: "#d32f2f" }}>
{error}
</li>
)}
{!isLoading && !error && suggestions.length === 0 && (
<li style={{ padding: "8px 12px", color: "#666" }}>
No suggestions found
</li>
)}
{!isLoading && !error && suggestions.map((suggestion, i) => {
const isHighlighted = i === highlightIndex;
return (
<li
key={suggestion.id}
id={`suggestion-${i}`}
role="option"
aria-selected={isHighlighted}
onMouseDown={(e) => {
// Prevent blur on input
e.preventDefault();
selectSuggestion(suggestion);
}}
onMouseEnter={() => setHighlightIndex(i)}
style={{
padding: "8px 12px",
backgroundColor: isHighlighted ? "#007acc" : "white",
color: isHighlighted ? "white" : "black",
cursor: "pointer",
}}
>
{renderSuggestion ? (
renderSuggestion(suggestion, isHighlighted)
) : (
highlightText(suggestion.text, input)
)}
</li>
);
})}
</ul>
)}
</div>
);
}Best Practices
Performance
- Debounce API calls to reduce server load
- Implement caching for frequently searched terms
- Use virtualization for large suggestion lists
- Optimize bundle size with code splitting
User Experience
- Provide loading states for better feedback
- Handle errors gracefully with fallback options
- Support keyboard navigation for accessibility
- Highlight matching text for clarity
Code Quality
- Write comprehensive tests for all interactions
- Use TypeScript for better type safety
- Follow accessibility guidelines (WCAG 2.1)
- Document the API clearly
Security
- Sanitize user input to prevent XSS
- Validate API responses before rendering
- Use HTTPS for API calls
- Implement rate limiting on the server
Interview Tips
What Interviewers Look For:
- Understanding of performance implications (debouncing, caching)
- Accessibility considerations (ARIA, keyboard navigation)
- Error handling strategies (network failures, empty states)
- Scalability thinking (large datasets, concurrent users)
Sample Questions:
- "How would you handle millions of suggestions?"
- "What if the API is slow or unreliable?"
- "How would you implement fuzzy search?"
- "How would you make this work offline?"
Key Points to Mention:
- Start with simple substring matching, then optimize
- Consider edge cases (empty results, loading states, errors)
- Think about accessibility from the beginning
- Plan for scalability (caching, virtualization, pagination)
Next: Google Sheets Design - Learn how to build a spreadsheet application with formulas and real-time collaboration.