Front-end Engineering Lab
RADIO FrameworkUI Components

Autocomplete/Search Component

System design for search with suggestions, debouncing, race conditions, and caching

Design an autocomplete UI component that allows users to enter a search term, see suggestions in a popup, and select a result.

Real-world examples: Google search bar, Facebook search (rich results), X search

R - Requirements

Key Questions:

  • What kind of results? (text, image, media - image + text)
  • What devices? (laptop, tablets, mobile)
  • Fuzzy search needed? (usually not for initial version)
  • Number of results to show? (5, 10, 20)
  • Minimum query length? (usually 3+ characters)

Common Answers:

  • Text, image, or media results
  • All devices (responsive)
  • No fuzzy search initially
  • 5-10 results typically

A - Architecture

High-Level Flow:

Component Responsibilities:

  • Input field UI - Handles user input, passes to controller
  • Results UI (Popup) - Receives results, displays, handles selection
  • Controller - Brain of component, coordinates all interactions, debounces, manages cache
  • Cache - Stores previous queries to avoid server requests (normalized object for O(1) lookup)

Data Flow:

  • Client → Server: Search query (debounced, min 3 chars)
  • Server → Client: Search results array
  • Client → Cache: Query → Results mapping (TTL-based expiration)

Relevant Content:

D - Data Model

Controller State:

  • Props/options (component API)
  • Current search string
  • Loading/error states

Cache Structure:

  • Normalized store (object keyed by query, not array) for O(1) lookup
  • Query → Results mapping
  • Cache duration/TTL

Implementation Example - Normalized Cache:

// ❌ Array-based cache (slow lookup)
const cache: Array<{ query: string; results: Result[] }> = [];
// O(n) lookup - slow for many queries

// ✅ Object-based cache (fast lookup)
const cache: Record<string, { results: Result[]; timestamp: number }> = {};
// O(1) lookup - fast even with many queries

function getCachedResults(query: string): Result[] | null {
  const cached = cache[query];
  if (!cached) return null;
  
  // Check TTL (e.g., 5 minutes)
  const isExpired = Date.now() - cached.timestamp > 5 * 60 * 1000;
  return isExpired ? null : cached.results;
}

Race Condition Handling:

  • Save results in object/map keyed by search query string
  • Only display results corresponding to current input value
  • Discard responses from outdated queries

Implementation Example - Race Condition:

let currentQuery = '';

async function search(query: string) {
  currentQuery = query; // Track current query
  
  // Check cache first
  const cached = getCachedResults(query);
  if (cached) {
    displayResults(cached);
    return;
  }
  
  // Fetch from server
  const results = await fetchResults(query);
  
  // Only display if this is still the current query
  if (currentQuery === query) {
    cache[query] = { results, timestamp: Date.now() };
    displayResults(results);
  }
  // If query changed, discard these results
}

Relevant Content:

I - Interface

API Design:

Basic API:

  • Number of results
  • API URL
  • Event listeners (input, focus, blur, change, select)
  • Customized rendering (theming, classnames, render function)

Advanced API:

  • Minimum query length (3+ characters)
  • Debounce duration (300ms typical)
  • API timeout duration
  • Cache options (initial results, source, merge function, duration)

UX Features:

  • Autofocus (if search page)
  • Loading spinner
  • Error states (with retry)
  • No network handling
  • Long string truncation (ellipsis)
  • Mobile-friendly (large tap targets, autocapitalize/autocomplete/autocorrect="off")
  • Global shortcut key (e.g., "/" like Facebook, X, YouTube)
  • Query results positioning (above input if no space below)

Accessibility:

  • ARIA combobox pattern
  • aria-label, aria-expanded, aria-haspopup
  • aria-autocomplete="list" (Facebook, X) or "both" (Google)
  • aria-live region for results
  • Keyboard navigation (Enter, Up/Down arrows, Escape)

Relevant Content:

O - Optimizations

Network:

  • Debouncing (300ms) - Don't search on every keystroke
  • Race condition handling - Query-keyed cache (better than timestamps)
  • Request deduplication - Same query = same result
  • Cache strategy - Normalized object for O(1) lookup

Performance:

  • Virtualized lists - For hundreds/thousands of results
  • Result caching - Avoid duplicate requests
  • Prefetch popular searches - Load before user types

UX:

  • Fuzzy search - Handle typos (Levenshtein distance, server-side)
  • Progressive enhancement - Works without JS

Relevant Content:

Implementation Checklist

  • MVC architecture (Input UI, Results UI, Controller, Cache)
  • Debounce search (300ms)
  • Minimum query length (3+ characters)
  • Race condition handling (query-keyed cache)
  • Request deduplication
  • Result caching (normalized object)
  • Virtualized list (if many results)
  • Keyboard navigation (Enter, arrows, Escape)
  • Accessibility (ARIA combobox, screen readers)
  • Mobile-friendly (tap targets, input attributes)
  • Error handling (timeout, retry, no network)
  • Global shortcut key ("/")

Common Pitfalls

Search on every keystroke → Too many requests
Debounce (300ms) to reduce requests

Race conditions → Show wrong results
Query-keyed cache, only show results for current input

No request deduplication → Same query = multiple requests
Cache requests, reuse results

Poor accessibility → Not usable with keyboard/screen readers
ARIA combobox pattern, keyboard navigation, aria-live

Real-World Comparison

Google: aria-autocomplete="both", uses <textarea>, within <form>
Facebook/X: aria-autocomplete="list", uses <input>, no form wrapper

Key Takeaway: No standardized ARIA practice - choose based on use case.

On this page