Build a debounced search input
mediumPerformanceHigh freq+180 XP
Wire a search input so that typing doesn't search on every keystroke — it should wait for a 300ms pause in typing before running the search.
Every keystroke inside that pause resets the wait — only the final value, after things go quiet, actually counts as a search.
Examples
// Type "a" (t=0)
// Type "ab" (t=50)
// Type "abc" (t=100)
// ... wait 300ms of silence ...
// → search-count = 1, last-query = "abc"
// Resume typing: "abcd" (t=500) → wait 300ms
// → search-count = 2, last-query = "abcd"Each keystroke restarts the 300ms clock; only the final value fires.
const timerRef = useRef(null);
function handleChange(e) {
const value = e.target.value;
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setLastQuery(value);
setCount((c) => c + 1);
}, 300);
}A ref holds the pending timer so each keystroke can cancel it. Capture `value` in the closure so the fired search uses the keystroke-current query.
What it has to do
- There is exactly one `<input id="search-input">`.
- An element `data-testid="search-count"` shows how many searches have actually run, starting at 0.
- An element `data-testid="last-query"` shows the text of the most recent completed search.
- A search only runs 300ms after the last keystroke — typing continuously must not increase the count until it stops.
Notes
- Keep the input itself uncontrolled (or controlled but updated every keystroke) — only the *search* is debounced, not the typing. Users expect the characters to appear instantly.
- If the search is a network call, also cancel the in-flight request on each new keystroke (AbortController) — otherwise a slow earlier request can overwrite the newer response.
- Cleanup the pending timer on unmount; a stale timeout firing on a dead component triggers React's "setState on unmounted" warning and leaks a reference to the old tree.
- A `useEffect` keyed on `query` is an equally valid implementation and composes better with React — the cleanup is the clearTimeout; the ref-based form here just avoids an extra re-render.
Stuck? The react course covers the ideas this problem is built on.
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); const [lastQuery, setLastQuery] = useState(""); function handleChange(e) { // TODO: only "search" (update lastQuery and increment count) 300ms // after the user stops typing — not on every keystroke. setLastQuery(e.target.value); setCount((c) => c + 1); } return ( <div style={{ fontFamily: "sans-serif", padding: 16 }}> <input id="search-input" onChange={handleChange} placeholder="Search..." style={{ padding: 8, width: "100%", boxSizing: "border-box" }} /> <p>Searches performed: <span data-testid="search-count">{count}</span></p> <p>Last query: <span data-testid="last-query">{lastQuery}</span></p> </div> ); }
- 1.There is exactly one `<input id="search-input">`.
- 2.An element `data-testid="search-count"` shows how many searches have actually run, starting at 0.
- 3.An element `data-testid="last-query"` shows the text of the most recent completed search.
- 4.A search only runs 300ms after the last keystroke — typing continuously must not increase the count until it stops.
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
5 tests · +180 XP
Discussion
Sign in to postSign in to join the conversation.Sign in
No comments yet. Be the first to say something.