All challenges
The Directory That Rewinds
API / Sync
Team directory search hits a live API. When you type quickly, results sometimes rewind to an older query. The list should always match the name in the box.
After you fix it
- Slow typing should show contacts whose names match the current search.
- A slower earlier request must not replace results after you type a longer query.
- Try “le”, then quickly “lean” — you should end on Leanne Graham, not a wider stale list.
import { useState, useEffect } from "react"; const DIRECTORY_URL = "https://jsonplaceholder.typicode.com/users"; async function fetchDirectory(term) { const q = term.trim().toLowerCase(); const res = await fetch(DIRECTORY_URL); if (!res.ok) throw new Error("Directory unavailable"); const users = await res.json(); const ms = q.length <= 2 ? 650 : q.length === 3 ? 300 : 80; await new Promise((r) => setTimeout(r, ms)); return users .filter((u) => u.name.toLowerCase().includes(q)) .slice(0, 6) .map((u) => ({ id: String(u.id), name: u.name })); } export default function App() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); useEffect(() => { const b = document.body; const pb = b.style.background; const pm = b.style.margin; b.style.background = "#fafafa"; b.style.margin = "0"; return () => { b.style.background = pb; b.style.margin = pm; }; }, []); // Loads team contacts from the public directory service. // Rows should match the current search text. useEffect(() => { const q = query.trim(); if (!q) { setResults([]); setLoading(false); setError(""); return; } setLoading(true); setError(""); const debounce = setTimeout(() => { fetchDirectory(q) .then((rows) => { setResults(rows); setLoading(false); }) .catch(() => { setError("Could not reach directory."); setLoading(false); }); }, 300); return () => clearTimeout(debounce); }, [query]); return ( <div style={styles.page}> <div style={styles.card}> <h1 style={styles.h1}>Team directory</h1> <p style={styles.sub}>Live lookup against the org contact API.</p> <input data-testid="search-input" type="search" placeholder="Name" value={query} onChange={(e) => setQuery(e.target.value)} style={styles.input} /> <p data-testid="search-status" style={styles.status}> {error || (loading ? "Searching…" : results.length ? results.length + " contact(s)" : query ? "No contacts" : "")} </p> <ul style={styles.list}> {results.map((c) => ( <li key={c.id} data-testid="result-row" style={styles.row}>{c.name}</li> ))} </ul> </div> </div> ); } const styles = { page: { display: "flex", alignItems: "center", justifyContent: "center", padding: "48px 24px", fontFamily: "'Segoe UI', system-ui, sans-serif" }, card: { background: "#fff", borderRadius: 16, padding: "28px 32px", boxShadow: "0 1px 3px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.04)", width: "100%", maxWidth: 400 }, h1: { margin: "0 0 6px", fontSize: 20, fontWeight: 700, color: "#1a1a2e" }, sub: { margin: "0 0 16px", fontSize: 13, color: "#666" }, input: { width: "100%", boxSizing: "border-box", padding: "10px 12px", fontSize: 15, borderRadius: 8, border: "1px solid #ddd", marginBottom: 10 }, status: { margin: "0 0 10px", fontSize: 12, color: "#666", minHeight: 16 }, list: { margin: 0, padding: 0, listStyle: "none" }, row: { padding: "8px 0", fontSize: 14, color: "#1a1a2e", borderBottom: "1px solid #eee" }, };