Skip to content
intermediatePhase 37 · Frontend Architecture

Search

Build search interfaces with debouncing, indexing, and result ranking.

45m
0 problems
Topic Progress0%

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

0/3solved
Build Search Component

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 needed
Search Testing

Write 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 tests
Search Performance

Optimize 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 analysis

Quiz

1. When should you use client-side search?

Question 1 options

2. Why debounce server search requests?

Question 2 options

3. What is the benefit of fuzzy search?

Question 3 options

4. What ARIA attributes should autocomplete use?

Question 4 options

5. How should you handle search with URL state?

Question 5 options

Flashcards

Question

When should you use client-side vs server-side search?

Answer

Client for small datasets (< 1000 items); server for large datasets or complex queries.

Question

What is debounce in search?

Answer

Delaying the search request until the user stops typing for a specified time.

Question

What ARIA roles are needed for autocomplete?

Answer

role="combobox", aria-expanded, aria-autocomplete="list", and role="listbox" for suggestions.

Question

Why sync search with URL params?

Answer

Enables sharing search results and supports browser back/forward navigation.

Question

What is Search?

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