React cookbook · Beginner
Debounce a search input in React
Search an API as the user types without firing a request on every keystroke or showing stale results.
Approach
Keep the input itself instant (controlled state), and derive a debounced value with a small hook. Use the debounced value in the query key so TanStack Query fetches only after typing pauses, cancels outdated requests and caches previous searches.
Solution
import { useEffect, useState } from "react";
import { useQuery, keepPreviousData } from "@tanstack/react-query";
function useDebouncedValue<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
export function ProductSearch() {
const [query, setQuery] = useState("");
const debounced = useDebouncedValue(query.trim(), 300);
const { data = [], isFetching } = useQuery({
queryKey: ["products", "search", debounced],
queryFn: ({ signal }) =>
fetch(`/api/products?q=${encodeURIComponent(debounced)}`, { signal }).then((r) => r.json() as Promise<Product[]>),
enabled: debounced.length >= 2,
placeholderData: keepPreviousData,
staleTime: 30_000,
});
return (
<div role="search">
<label htmlFor="product-search">Search products</label>
<input id="product-search" type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
{isFetching && <span aria-live="polite">Searching…</span>}
<ul>{data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
</div>
);
}
Pitfalls
- Debouncing the input's own state (typing feels laggy).
- Fetching in
useEffectwithout cancellation — out-of-order responses show stale results. - Not encoding the query parameter.
Variations
- Throttle instead of debounce for continuous events (scroll, resize).
- Use
useDeferredValuefor client-side filtering of local data.
Further reading: TanStack Query: Query cancellation
Reference: https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation
More React recipes
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status
- Protect routes in a React SPA