React - Search (AJAX)
Overview
Estimated time: 25–35 minutes
Build a debounced search box that queries an API, cancels stale requests, and renders results as you type.
Try it: Debounced Search
View source
const { useEffect, useRef, useState } = React;
function Search(){
const [q, setQ] = useState('');
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const tRef = useRef(null);
const ctlRef = useRef(null);
useEffect(() => {
if (tRef.current) clearTimeout(tRef.current);
if (!q) { setItems([]); setError(''); setLoading(false); return; }
tRef.current = setTimeout(async () => {
try {
setLoading(true); setError('');
if (ctlRef.current) ctlRef.current.abort();
ctlRef.current = new AbortController();
const res = await fetch(`https://dummyjson.com/products/search?q=${encodeURIComponent(q)}`, { signal: ctlRef.current.signal });
if (!res.ok) throw new Error('Network error');
const data = await res.json();
setItems(data.products || []);
} catch (e) {
if (e.name !== 'AbortError') setError(e.message || 'Error');
} finally {
setLoading(false);
}
}, 400);
return () => { if (tRef.current) clearTimeout(tRef.current); };
}, [q]);
return (
<div>
<input placeholder="Search products" value={q} onChange={e => setQ(e.target.value)} />
{loading && <span style={{marginLeft:8}}>Loading...</span>}
{error && <div style={{color:'salmon'}}>{error}</div>}
<ul>{items.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-search')).render(<Search />);
Syntax primer
- Debounce input by delaying the fetch until typing pauses.
- Cancel previous fetch with
AbortController
to avoid race conditions.
Common pitfalls
- Firing a request on every keystroke without debounce; wastes bandwidth and thrashes UI.
Exercises
- Show a result count and no-results message.
- Highlight query matches in results.