Debounced product search
React · React · advanced · greenfield
Adds the ProductSearch component: a debounced effect issues the request 300 ms after the last keystroke, an AbortController cancels the in-flight request whenever the query changes or the component unmounts, and an aborted-signal guard after the await keeps any cancelled or already-resolved stale response from touching state. Loading, error and empty states are all rendered.
Requirements
- Add live search to the product catalog: as the user types, query the search API and render the results.
- Debounce requests by 300 ms — no request while the user is still typing.
- A stale response must never overwrite the results of a newer query, regardless of network timing.
- An empty (or whitespace-only) box shows no results and makes no request.
- While the user is typing a new query or its request is loading, keep the previous results visible; they are replaced when the new response arrives.
- Handle loading and error states, announced to assistive technology; `searchProducts(query, { signal })` resolves with an array of products with unique, stable `id`s, supports cancellation via `AbortSignal`, and rejects when cancelled.
Files touched
- src/components/ProductSearch.jsx
--- src/components/ProductSearch.jsx
+import { useEffect, useState } from 'react';
+import { searchProducts } from '../api/search';
+
+const DEBOUNCE_MS = 300;
+
+function ProductSearch() {
+ const [query, setQuery] = useState('');
+ const [results, setResults] = useState([]);
+ const [status, setStatus] = useState('idle');
+
+ useEffect(() => {
+ const trimmed = query.trim();
+ if (trimmed === '') {
+ setResults([]);
+ setStatus('idle');
+ return undefined;
+ }
+ const controller = new AbortController();
+ const timer = setTimeout(async () => {
+ setStatus('loading');
+ try {
+ const found = await searchProducts(trimmed, { signal: controller.signal });
+ if (controller.signal.aborted) return;
+ setResults(found);
+ setStatus('done');
+ } catch {
+ if (controller.signal.aborted) return;
+ setStatus('error');
+ }