patterntypescriptreactTip
Debounced search input — avoiding excessive API calls on keypress
Viewed 0 times
debouncesearch inputuseDebouncekeypress throttlesetTimeout cleanupsearch APIreact query search
Problem
A search input that fires an API request on every keystroke produces dozens of requests for a short query like 'typescript'. Most requests are for intermediate states the user never intends to search for, wasting bandwidth and server resources.
Solution
Maintain a debounced value that updates only after the user pauses typing:
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 400);
const { data, isLoading } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => fetch(
enabled: debouncedQuery.length >= 2, // minimum 2 characters
});
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
aria-label="Search"
/>
{isLoading && <Spinner />}
{data?.results.map((r: { id: number; title: string }) => (
<div key={r.id}>{r.title}</div>
))}
</div>
);
}
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 400);
const { data, isLoading } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => fetch(
/api/search?q=${debouncedQuery}).then((r) => r.json()),enabled: debouncedQuery.length >= 2, // minimum 2 characters
});
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
aria-label="Search"
/>
{isLoading && <Spinner />}
{data?.results.map((r: { id: number; title: string }) => (
<div key={r.id}>{r.title}</div>
))}
</div>
);
}
Why
The debounce hook clears and resets a setTimeout on each value change. Only when the user stops typing for delay milliseconds does the debounced value update, triggering a single query instead of one per keystroke.
Gotchas
- The cleanup function (clearTimeout) prevents the timeout from firing after the component unmounts
- TanStack Query deduplicates requests with the same key — even without debouncing, rapid key changes that resolve to the same key share one request
- Choose delay based on typical network latency: 300-500ms for autocomplete, 500-800ms for full search
- For TypeScript, useDebounce<T> infers the type from the initial value — no explicit annotation needed in most cases
Revisions (0)
No revisions yet.