AniUI Academy

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.
Asked atMetaGoogleUberNetflixStripe

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>
  );
}

5 tests · +180 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.