Advanced Autocomplete Search Component
Master machine coding by creating a production-ready autocomplete search with debouncing, keyboard navigation, virtual scrolling, and real-time suggestions.
Machine Coding Challenge: Advanced Autocomplete Search
Create a production-ready autocomplete search component that demonstrates advanced React patterns, performance optimization, and exceptional user experience.
Problem Statement
Design and implement an autocomplete search component with the following requirements:
✅ Core Features (Must Have)
- Real-time search suggestions as user types
- Debounced input to prevent excessive API calls
- Keyboard navigation (arrow keys, enter, escape)
- Click to select suggestions
- Loading states and error handling
- Responsive design
Advanced Features (Nice to Have)
- Virtual scrolling for large suggestion lists
- Search highlighting in suggestions
- Recent searches with localStorage persistence
- Multi-select functionality
- Custom suggestion rendering
- Accessibility compliance (ARIA labels, screen reader support)
UI/UX Requirements
- Clean, modern dropdown interface
- Smooth animations and transitions
- Focus management and keyboard shortcuts
- Empty state and no results handling
- Mobile-friendly touch interactions
🧠 Clarifying Questions & Scope
Functional Requirements
- Data Source: Mock API or real API integration?
- Suggestion Limit: How many suggestions to show? (10, 20, 50?)
- Search Logic: Exact match, fuzzy search, or prefix matching?
- Multi-select: Should users be able to select multiple items?
- Recent Searches: Should we persist and show recent searches?
Technical Constraints
- Time Limit: 2-3 hours for implementation
- Framework: React with hooks
- Styling: Tailwind CSS or custom CSS?
- Performance: Handle 1000+ suggestions efficiently?
Success Criteria
- Smooth, responsive user experience
- Proper error handling and edge cases
- Clean, maintainable code structure
- Performance optimization for large datasets
🏗️ High-Level Architecture
1. Component Structure
AutocompleteSearch/
├── AutocompleteInput/ # Main input with dropdown
│ ├── SearchInput/ # Text input with debouncing
│ ├── SuggestionsList/ # Dropdown with suggestions
│ │ ├── SuggestionItem/ # Individual suggestion
│ │ └── VirtualList/ # Performance optimization
│ └── RecentSearches/ # Recent searches section
├── SearchHighlight/ # Highlight matching text
├── LoadingSpinner/ # Loading state
└── ErrorBoundary/ # Error handling2. State Management Strategy
interface Suggestion {
id: string;
label: string;
value: string;
category?: string;
icon?: string;
}
interface AutocompleteState {
query: string;
suggestions: Suggestion[];
selectedIndex: number;
isOpen: boolean;
isLoading: boolean;
error: string | null;
recentSearches: string[];
selectedItems: Suggestion[];
}3. Data Flow
User Input → Debounce → API Call → Filter Results → Update Suggestions → Render
↓
Keyboard Navigation ← Focus Management ← Accessibility ← User Experience🛠️ Step-by-Step Implementation
Step 1: Core Autocomplete Component
// components/AutocompleteSearch.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useDebounce } from '../hooks/useDebounce';
import { useKeyboardNavigation } from '../hooks/useKeyboardNavigation';
import { SearchInput } from './SearchInput';
import { SuggestionsList } from './SuggestionsList';
import { LoadingSpinner } from './LoadingSpinner';
interface AutocompleteSearchProps {
fetchSuggestions: (query: string) => Promise<Suggestion[]>;
onSelect: (suggestion: Suggestion) => void;
placeholder?: string;
debounceMs?: number;
maxSuggestions?: number;
multiSelect?: boolean;
}
const AutocompleteSearch: React.FC<AutocompleteSearchProps> = ({
fetchSuggestions,
onSelect,
placeholder = "Search...",
debounceMs = 300,
maxSuggestions = 10,
multiSelect = false
}) => {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const debouncedQuery = useDebounce(query, debounceMs);
// Fetch suggestions when query changes
useEffect(() => {
if (!debouncedQuery.trim()) {
setSuggestions([]);
setIsOpen(false);
return;
}
const loadSuggestions = async () => {
setIsLoading(true);
setError(null);
try {
const results = await fetchSuggestions(debouncedQuery);
setSuggestions(results.slice(0, maxSuggestions));
setIsOpen(true);
setSelectedIndex(-1);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load suggestions');
setSuggestions([]);
} finally {
setIsLoading(false);
}
};
loadSuggestions();
}, [debouncedQuery, fetchSuggestions, maxSuggestions]);
// Keyboard navigation
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev =>
prev < suggestions.length - 1 ? prev + 1 : 0
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev =>
prev > 0 ? prev - 1 : suggestions.length - 1
);
break;
case 'Enter':
e.preventDefault();
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
handleSelect(suggestions[selectedIndex]);
}
break;
case 'Escape':
e.preventDefault();
setIsOpen(false);
setSelectedIndex(-1);
inputRef.current?.blur();
break;
}
}, [suggestions, selectedIndex]);
const handleSelect = useCallback((suggestion: Suggestion) => {
onSelect(suggestion);
setQuery(suggestion.label);
setIsOpen(false);
setSelectedIndex(-1);
inputRef.current?.blur();
}, [onSelect]);
const handleInputChange = useCallback((value: string) => {
setQuery(value);
if (!value.trim()) {
setIsOpen(false);
setSuggestions([]);
}
}, []);
const handleInputFocus = useCallback(() => {
if (suggestions.length > 0) {
setIsOpen(true);
}
}, [suggestions.length]);
const handleClickOutside = useCallback((e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsOpen(false);
setSelectedIndex(-1);
}
}, []);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [handleClickOutside]);
return (
<div ref={containerRef} className="autocomplete-container relative">
<SearchInput
ref={inputRef}
value={query}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={handleInputFocus}
placeholder={placeholder}
isLoading={isLoading}
aria-expanded={isOpen}
aria-autocomplete="list"
aria-activedescendant={
selectedIndex >= 0 ? `suggestion-${selectedIndex}` : undefined
}
/>
{isOpen && (
<SuggestionsList
suggestions={suggestions}
selectedIndex={selectedIndex}
onSelect={handleSelect}
isLoading={isLoading}
error={error}
query={query}
/>
)}
</div>
);
};Step 2: Custom Hooks Implementation
// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// hooks/useRecentSearches.ts
import { useState, useEffect } from 'react';
export function useRecentSearches(key: string, maxItems: number = 10) {
const [recentSearches, setRecentSearches] = useState<string[]>([]);
useEffect(() => {
const saved = localStorage.getItem(key);
if (saved) {
try {
setRecentSearches(JSON.parse(saved));
} catch {
setRecentSearches([]);
}
}
}, [key]);
const addSearch = (search: string) => {
if (!search.trim()) return;
setRecentSearches(prev => {
const filtered = prev.filter(item => item !== search);
const updated = [search, ...filtered].slice(0, maxItems);
localStorage.setItem(key, JSON.stringify(updated));
return updated;
});
};
const clearSearches = () => {
setRecentSearches([]);
localStorage.removeItem(key);
};
return { recentSearches, addSearch, clearSearches };
}
// hooks/useVirtualization.ts
import { useState, useMemo, useCallback } from 'react';
interface UseVirtualizationOptions {
items: any[];
itemHeight: number;
containerHeight: number;
overscan?: number;
}
export function useVirtualization({
items,
itemHeight,
containerHeight,
overscan = 5
}: UseVirtualizationOptions) {
const [scrollTop, setScrollTop] = useState(0);
const virtualItems = useMemo(() => {
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
return items.slice(startIndex, endIndex + 1).map((item, index) => ({
...item,
index: startIndex + index,
style: {
position: 'absolute' as const,
top: (startIndex + index) * itemHeight,
height: itemHeight,
width: '100%'
}
}));
}, [items, itemHeight, containerHeight, scrollTop, overscan]);
const totalHeight = items.length * itemHeight;
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
return {
virtualItems,
totalHeight,
handleScroll
};
}Step 3: Supporting Components
// components/SearchInput.tsx
import React, { forwardRef } from 'react';
import { SearchIcon, XIcon } from 'lucide-react';
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
onFocus: () => void;
placeholder?: string;
isLoading?: boolean;
'aria-expanded'?: boolean;
'aria-autocomplete'?: string;
'aria-activedescendant'?: string;
}
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
({ value, onChange, onKeyDown, onFocus, placeholder, isLoading, ...ariaProps }, ref) => {
return (
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" />
</div>
<input
ref={ref}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={onKeyDown}
onFocus={onFocus}
placeholder={placeholder}
className="block w-full pl-10 pr-10 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
{...ariaProps}
/>
{isLoading && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
</div>
)}
{value && !isLoading && (
<button
onClick={() => onChange('')}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<XIcon className="h-4 w-4 text-gray-400 hover:text-gray-600" />
</button>
)}
</div>
);
}
);
// components/SuggestionsList.tsx
import React from 'react';
import { SuggestionItem } from './SuggestionItem';
import { VirtualList } from './VirtualList';
interface SuggestionsListProps {
suggestions: Suggestion[];
selectedIndex: number;
onSelect: (suggestion: Suggestion) => void;
isLoading: boolean;
error: string | null;
query: string;
}
const SuggestionsList: React.FC<SuggestionsListProps> = ({
suggestions,
selectedIndex,
onSelect,
isLoading,
error,
query
}) => {
if (isLoading) {
return (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg">
<div className="p-4 text-center text-gray-500">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto mb-2" />
Loading suggestions...
</div>
</div>
);
}
if (error) {
return (
<div className="absolute z-10 w-full mt-1 bg-white border border-red-300 rounded-lg shadow-lg">
<div className="p-4 text-center text-red-600">
<p>Error: {error}</p>
<button className="mt-2 text-blue-600 hover:text-blue-800">
Try again
</button>
</div>
</div>
);
}
if (suggestions.length === 0 && query.trim()) {
return (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg">
<div className="p-4 text-center text-gray-500">
No suggestions found
</div>
</div>
);
}
return (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-hidden">
<VirtualList
items={suggestions}
itemHeight={48}
containerHeight={240}
renderItem={(suggestion, index) => (
<SuggestionItem
key={suggestion.id}
suggestion={suggestion}
isSelected={index === selectedIndex}
onSelect={onSelect}
query={query}
/>
)}
/>
</div>
);
};
// components/SuggestionItem.tsx
import React from 'react';
import { SearchHighlight } from './SearchHighlight';
interface SuggestionItemProps {
suggestion: Suggestion;
isSelected: boolean;
onSelect: (suggestion: Suggestion) => void;
query: string;
}
const SuggestionItem: React.FC<SuggestionItemProps> = ({
suggestion,
isSelected,
onSelect,
query
}) => {
return (
<div
className={`px-4 py-3 cursor-pointer transition-colors ${
isSelected
? 'bg-blue-50 text-blue-900'
: 'hover:bg-gray-50 text-gray-900'
}`}
onClick={() => onSelect(suggestion)}
role="option"
aria-selected={isSelected}
>
<div className="flex items-center gap-3">
{suggestion.icon && (
<div className="w-5 h-5 text-gray-400">
{suggestion.icon}
</div>
)}
<div className="flex-1 min-w-0">
<div className="font-medium">
<SearchHighlight text={suggestion.label} query={query} />
</div>
{suggestion.category && (
<div className="text-sm text-gray-500">
{suggestion.category}
</div>
)}
</div>
</div>
</div>
);
};Step 4: Advanced Features
// components/SearchHighlight.tsx
import React from 'react';
interface SearchHighlightProps {
text: string;
query: string;
}
const SearchHighlight: React.FC<SearchHighlightProps> = ({ text, query }) => {
if (!query.trim()) {
return <span>{text}</span>;
}
const regex = new RegExp(`(${query})`, 'gi');
const parts = text.split(regex);
return (
<span>
{parts.map((part, index) =>
regex.test(part) ? (
<mark key={index} className="bg-yellow-200 px-1 rounded">
{part}
</mark>
) : (
<span key={index}>{part}</span>
)
)}
</span>
);
};
// components/VirtualList.tsx
import React, { useRef } from 'react';
import { useVirtualization } from '../hooks/useVirtualization';
interface VirtualListProps {
items: any[];
itemHeight: number;
containerHeight: number;
renderItem: (item: any, index: number) => React.ReactNode;
}
const VirtualList: React.FC<VirtualListProps> = ({
items,
itemHeight,
containerHeight,
renderItem
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const { virtualItems, totalHeight, handleScroll } = useVirtualization({
items,
itemHeight,
containerHeight,
overscan: 10
});
return (
<div
ref={containerRef}
className="overflow-auto"
style={{ height: containerHeight }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{virtualItems.map(({ index, style, ...item }) => (
<div key={item.id || index} style={style}>
{renderItem(item, index)}
</div>
))}
</div>
</div>
);
};UI/UX Enhancements
Responsive Design
/* Tailwind classes for responsive autocomplete */
.autocomplete-container {
@apply relative w-full max-w-md mx-auto;
}
@media (max-width: 640px) {
.autocomplete-container {
@apply max-w-full;
}
.suggestions-list {
@apply max-h-48;
}
}Accessibility Features
// Enhanced accessibility with ARIA labels
const AutocompleteSearch: React.FC<AutocompleteSearchProps> = (props) => {
return (
<div
className="autocomplete-container"
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-owns="suggestions-list"
>
<SearchInput
aria-autocomplete="list"
aria-activedescendant={
selectedIndex >= 0 ? `suggestion-${selectedIndex}` : undefined
}
aria-controls="suggestions-list"
{...inputProps}
/>
{isOpen && (
<ul
id="suggestions-list"
role="listbox"
aria-label="Search suggestions"
className="suggestions-list"
>
{suggestions.map((suggestion, index) => (
<li
key={suggestion.id}
id={`suggestion-${index}`}
role="option"
aria-selected={index === selectedIndex}
>
<SuggestionItem
suggestion={suggestion}
isSelected={index === selectedIndex}
onSelect={handleSelect}
query={query}
/>
</li>
))}
</ul>
)}
</div>
);
};Performance Optimizations
Memoization and Optimization
// Optimized suggestion rendering with React.memo
const SuggestionItem = React.memo<SuggestionItemProps>(({
suggestion,
isSelected,
onSelect,
query
}) => {
const handleClick = useCallback(() => {
onSelect(suggestion);
}, [onSelect, suggestion]);
return (
<div
className={`suggestion-item ${isSelected ? 'selected' : ''}`}
onClick={handleClick}
>
<SearchHighlight text={suggestion.label} query={query} />
</div>
);
});
// Optimized search with useMemo
const filteredSuggestions = useMemo(() => {
if (!query.trim()) return suggestions;
return suggestions.filter(suggestion =>
suggestion.label.toLowerCase().includes(query.toLowerCase()) ||
suggestion.category?.toLowerCase().includes(query.toLowerCase())
);
}, [suggestions, query]);Caching and Preloading
// hooks/useSearchCache.ts
import { useRef, useCallback } from 'react';
export function useSearchCache() {
const cache = useRef<Map<string, Suggestion[]>>(new Map());
const get = useCallback((key: string) => {
return cache.current.get(key);
}, []);
const set = useCallback((key: string, suggestions: Suggestion[]) => {
cache.current.set(key, suggestions);
}, []);
const clear = useCallback(() => {
cache.current.clear();
}, []);
return { get, set, clear };
}
// Enhanced autocomplete with caching
const AutocompleteSearch: React.FC<AutocompleteSearchProps> = (props) => {
const { get, set } = useSearchCache();
const loadSuggestions = useCallback(async (query: string) => {
// Check cache first
const cached = get(query);
if (cached) {
setSuggestions(cached);
return;
}
// Fetch from API
const results = await props.fetchSuggestions(query);
set(query, results); // Cache results
setSuggestions(results);
}, [get, set, props.fetchSuggestions]);
};Key Takeaways
What You've Learned
- Performance Optimization: Debouncing, virtualization, and caching
- User Experience: Keyboard navigation and accessibility
- State Management: Complex search states and error handling
- Component Architecture: Modular and reusable design
- Real-world Patterns: Production-ready implementation
Common Pitfalls to Avoid
- ❌ No Debouncing: Excessive API calls on every keystroke
- ❌ Poor Accessibility: Missing keyboard navigation and ARIA labels
- ❌ Memory Leaks: Not cleaning up event listeners and timers
- ❌ Bad UX: No loading states or error handling
- ❌ Performance Issues: Rendering all suggestions without virtualization
Next Steps
- Implement advanced search algorithms (fuzzy search, typo tolerance)
- Add search analytics and user behavior tracking
- Integrate with real search APIs (Algolia, Elasticsearch)
- Add voice search capabilities
- Implement search suggestions and autocomplete
This autocomplete search component demonstrates advanced React patterns and real-world considerations. The skills you've learned here are essential for building responsive, user-friendly search experiences that scale with your application!
Frontend Machine Coding: Build a Dynamic Accordion Component
Learn how to build a flexible accordion component with smooth animations, accessibility features, and configurable behavior using React
Building a Responsive Carousel Component in React
Learn how to build a fully functional, responsive carousel component from scratch in React with autoplay, looping, and responsive design features.