Client-Side Search
Client-Side Search
Search through already-loaded data on the client.
Basic Client Search
// hooks/useClientSearch.js
function useClientSearch(items, searchFields, query) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query.trim()) {
setResults(items);
return;
}
const lowerQuery = query.toLowerCase();
const filtered = items.filter(item =>
searchFields.some(field => {
const value = field.split('.').reduce((obj, key) => obj?.[key], item);
return String(value).toLowerCase().includes(lowerQuery);
})
);
setResults(filtered);
}, [items, searchFields, query]);
return results;
}
// Component
function ProductSearch({ products }) {
const [query, setQuery] = useState('');
const results = useClientSearch(products, ['name', 'description', 'category'], query);
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
<ProductList products={results} />
</div>
);
}
Fuzzy Search
import Fuse from 'fuse.js';
function useFuzzySearch(items, options = {}) {
const fuse = useMemo(() => {
return new Fuse(items, {
keys: ['name', 'description'],
threshold: 0.3,
...options,
});
}, [items, options]);
const search = useCallback((query) => {
if (!query) return items;
return fuse.search(query).map(result => result.item);
}, [fuse, items]);
return search;
}
// Usage
function FuzzyProductSearch({ products }) {
const [query, setQuery] = useState('');
const fuzzySearch = useFuzzySearch(products);
const results = fuzzySearch(query);
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
<ProductList products={results} />
</div>
);
}
Search with Highlighting
function SearchHighlight({ text, query }) {
if (!query) return <span>{text}</span>;
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
const parts = text.split(regex);
return (
<span>
{parts.map((part, i) =>
regex.test(part) ? (
<mark key={i}>{part}</mark>
) : (
part
)
)}
</span>
);
}
// Usage
function ProductCard({ product, searchQuery }) {
return (
<div>
<h3>
<SearchHighlight text={product.name} query={searchQuery} />
</h3>
<p>
<SearchHighlight text={product.description} query={searchQuery} />
</p>
</div>
);
}
When to Use Client Search
- Small datasets (< 1000 items)
- Data already loaded on client
- Simple text matching
- No server dependency needed
Server-Side Search
Server-Side Search
Delegate search to the server for large datasets.
Debounced Server Search
// hooks/useServerSearch.js
function useServerSearch({ endpoint, debounceMs = 300 }) {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
const debounceRef = useRef(null);
useEffect(() => {
debounceRef.current = setTimeout(() => {
setDebouncedQuery(query);
}, debounceMs);
return () => clearTimeout(debounceRef.current);
}, [query, debounceMs]);
const { data, isLoading, error } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => fetch(`${endpoint}?q=${encodeURIComponent(debouncedQuery)}`).then(r => r.json()),
enabled: debouncedQuery.length > 0,
});
return {
query,
setQuery,
results: data?.items || [],
isLoading,
error,
};
}
// Component
function ProductSearch() {
const { query, setQuery, results, isLoading } = useServerSearch({
endpoint: '/api/products/search',
});
return (
<div>
<div className="search-input-wrapper">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{isLoading && <Spinner size="sm" />}
</div>
<ProductList products={results} />
</div>
);
}
Search API Pattern
// Server endpoint pattern
// GET /api/search?q=phone&filters=category:electronics&sort=relevance&page=1
// Client API call
async function searchProducts(params) {
const query = new URLSearchParams({
q: params.query,
filters: JSON.stringify(params.filters),
sort: params.sort,
page: params.page,
limit: params.limit,
});
const response = await fetch(`/api/products/search?${query}`);
return response.json();
}
Search with Filters
function SearchWithFilters() {
const [query, setQuery] = useState('');
const [filters, setFilters] = useState({
category: '',
priceRange: [0, 1000],
inStock: false,
});
const { data, isLoading } = useQuery({
queryKey: ['search', query, filters],
queryFn: () => searchProducts({ query, filters }),
enabled: query.length > 0,
});
return (
<div className="search-page">
<SearchBar value={query} onChange={setQuery} />
<FilterPanel filters={filters} onChange={setFilters} />
<SearchResults
results={data?.items || []}
total={data?.total || 0}
isLoading={isLoading}
/>
</div>
);
}
Performance Tips
- Debounce user input (300-500ms)
- Use loading indicators
- Implement request cancellation
- Cache recent searches
- Show search suggestions
Search UI Patterns
Search UI Patterns
Autocomplete
function Autocomplete({ onSelect, fetchSuggestions }) {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
useEffect(() => {
if (query.length < 2) {
setSuggestions([]);
return;
}
const fetchTimeout = setTimeout(async () => {
const results = await fetchSuggestions(query);
setSuggestions(results);
setIsOpen(true);
}, 200);
return () => clearTimeout(fetchTimeout);
}, [query, fetchSuggestions]);
const handleKeyDown = (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setActiveIndex(prev => Math.min(prev + 1, suggestions.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, -1));
} else if (e.key === 'Enter' && activeIndex >= 0) {
e.preventDefault();
onSelect(suggestions[activeIndex]);
setIsOpen(false);
} else if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div className="autocomplete">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => setIsOpen(true)}
role="combobox"
aria-expanded={isOpen}
aria-autocomplete="list"
/>
{isOpen && suggestions.length > 0 && (
<ul role="listbox" className="suggestions-list">
{suggestions.map((suggestion, index) => (
<li
key={suggestion.id}
role="option"
aria-selected={index === activeIndex}
className={index === activeIndex ? 'active' : ''}
onClick={() => {
onSelect(suggestion);
setIsOpen(false);
}}
>
{suggestion.name}
</li>
))}
</ul>
)}
</div>
);
}
Recent Searches
function useRecentSearches(key = 'recentSearches', maxItems = 5) {
const [recentSearches, setRecentSearches] = useState(() => {
try {
return JSON.parse(localStorage.getItem(key)) || [];
} catch {
return [];
}
});
const addSearch = useCallback((query) => {
setRecentSearches(prev => {
const filtered = prev.filter(s => s !== query);
const updated = [query, ...filtered].slice(0, maxItems);
localStorage.setItem(key, JSON.stringify(updated));
return updated;
});
}, [key, maxItems]);
const removeSearch = useCallback((query) => {
setRecentSearches(prev => {
const updated = prev.filter(s => s !== query);
localStorage.setItem(key, JSON.stringify(updated));
return updated;
});
}, [key]);
return { recentSearches, addSearch, removeSearch };
}
Search Results Layout
function SearchResults({ results, total, isLoading, query }) {
if (isLoading) {
return <SearchResultsSkeleton />;
}
if (results.length === 0) {
return (
<div className="no-results">
<h3>No results found</h3>
<p>Try adjusting your search or filters</p>
</div>
);
}
return (
<div className="search-results">
<div className="results-header">
<p>{total} results for "{query}"</p>
<SortDropdown />
</div>
<div className="results-grid">
{results.map(item => (
<ProductCard key={item.id} product={item} query={query} />
))}
</div>
</div>
);
}
Practice Problems
Create a reusable React component implementing Search. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Search using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Search for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. When should you use client-side search?
2. Why debounce server search requests?
3. What is the benefit of fuzzy search?
4. What ARIA attributes should autocomplete use?
5. How should you handle search with URL state?
Flashcards
Question
When should you use client-side vs server-side search?
Click to reveal answer
Answer
Client for small datasets (< 1000 items); server for large datasets or complex queries.
Question
What is debounce in search?
Click to reveal answer
Answer
Delaying the search request until the user stops typing for a specified time.
Question
What ARIA roles are needed for autocomplete?
Click to reveal answer
Answer
role="combobox", aria-expanded, aria-autocomplete="list", and role="listbox" for suggestions.
Question
Why sync search with URL params?
Click to reveal answer
Answer
Enables sharing search results and supports browser back/forward navigation.
Question
What is Search?
Click to reveal answer
Answer
Search is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Client-side search is best for small, already-loaded datasets
- 2.Debounce server search requests to reduce API calls
- 3.Implement proper ARIA attributes for search accessibility
- 4.Sync search state with URL for sharing and navigation
- 5.Show loading indicators and handle empty results gracefully
Interview Tips
- •Explain when to use client vs server search
- •Describe the debounce pattern and why it's important
- •Know how to make search accessible with ARIA
Cheat Sheet
Search Cheat Sheet
Client Search
const results = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
Server Search
useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => searchAPI(debouncedQuery)
});
Debounce
useEffect(() => {
const timer = setTimeout(() => setDebounced(query), 300);
return () => clearTimeout(timer);
}, [query]);
UI Patterns
- Autocomplete with ARIA roles
- Recent searches in localStorage
- Search highlighting
- Loading states