redline
← arena

Nothing wrong with it

PrincipalTypeScriptReactNext.js
UX-330the ticket

Debounce the project search box

The search input fires a request on every keystroke. Debounce it to ~250ms and make sure a slow response from an old query can't overwrite the results of a newer one.

Clear the results when the box is emptied.

the agent’s descriptionagent · opus-class model

Debounced with a 250ms timer inside the effect, and used an AbortController so an in-flight request is cancelled the moment the query changes — that also removes the out-of-order response problem, since a superseded request can never resolve. The cleanup clears the timer and aborts on every change and on unmount.

Empty query short-circuits to an empty result list without hitting the network.

Written by the agent that opened the PR. Fluent, specific, and not evidence of anything.

src/components/project-search.tsx65 lines
1"use client";
2
3import { useEffect, useState } from "react";
4
5type Result = { id: string; title: string };
6
7export function ProjectSearch() {
8 const [query, setQuery] = useState("");
9 const [results, setResults] = useState<Result[]>([]);
10 const [pending, setPending] = useState(false);
11
12 useEffect(() => {
13 const trimmed = query.trim();
14
15 if (trimmed === "") {
16 setResults([]);
17 setPending(false);
18 return;
19 }
20
21 const controller = new AbortController();
22 setPending(true);
23
24 const timer = setTimeout(() => {
25 fetch("/api/search?q=" + encodeURIComponent(trimmed), {
26 signal: controller.signal,
27 })
28 .then((res) => {
29 if (!res.ok) throw new Error("search failed: " + res.status);
30 return res.json();
31 })
32 .then((data: { results: Result[] }) => {
33 setResults(data.results);
34 setPending(false);
35 })
36 .catch((err: Error) => {
37 if (err.name === "AbortError") return;
38 setResults([]);
39 setPending(false);
40 });
41 }, 250);
42
43 return () => {
44 clearTimeout(timer);
45 controller.abort();
46 };
47 }, [query]);
48
49 return (
50 <div>
51 <input
52 value={query}
53 onChange={(event) => setQuery(event.target.value)}
54 placeholder="Search projects"
55 aria-label="Search projects"
56 />
57 {pending ? <p role="status">Searching...</p> : null}
58 <ul>
59 {results.map((result) => (
60 <li key={result.id}>{result.title}</li>
61 ))}
62 </ul>
63 </div>
64 );
65}