Exploration · Acme web client
Three ways to implement debounced search
Prompt
Show me three different ways to implement debounced search for the task
filter input in our React codebase, with tradeoffs for each.
01Inline useEffect + setTimeout
Debounce logic lives directly inside the component that owns the input.
export function TaskSearch() { const [draft, setDraft] = useState(''); const [query, setQuery] = useState(''); useEffect(() => { const id = setTimeout(() => setQuery(draft), 300); return () => clearTimeout(id); }, [draft]); const { data } = useTasks({ search: query }); return ( <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Filter tasks…" /> ); }
Pro
Con
Zero new abstractions to learn
Logic duplicated everywhere search exists
Easy to step through in devtools
Two pieces of state for one conceptual value
No dependency or bundle change
Delay constant is buried in component body
Bundle impact: +0 kb
Testability: medium
Reuse: low
SSR safe: yes
02Custom useDebounce hook
Extract the timer into a shared hook under src/hooks/.
// src/hooks/useDebounce.ts export function useDebounce<T>(value: T, ms = 300): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), ms); return () => clearTimeout(id); }, [value, ms]); return debounced; } // TaskSearch.tsx const [draft, setDraft] = useState(''); const query = useDebounce(draft, 300); const { data } = useTasks({ search: query });
Pro
Con
Single import reused across filter, command bar, board search
One more file to maintain and document
Trivial to unit test with fake timers
Generic
T hides intent slightlyDelay is a visible, tunable argument
Still re-renders on every keystroke
Bundle impact: +0.2 kb
Testability: high
Reuse: high
SSR safe: yes
03Tiny external library
Adopt use-debounce for both values and callbacks.
import { useDebouncedCallback } from 'use-debounce'; export function TaskSearch() { const [query, setQuery] = useState(''); const onChange = useDebouncedCallback( (next: string) => setQuery(next), 300, { leading: false, maxWait: 1000 }, ); const { data } = useTasks({ search: query }); return ( <input defaultValue="" onChange={(e) => onChange(e.target.value)} /> ); }
Pro
Con
leading / trailing / maxWait handled for us
New runtime dependency to audit and update
Callback form skips intermediate re-renders
Uncontrolled input diverges from Acme form patterns
Well-tested edge cases (unmount, flush, cancel)
~1.4 kb gzipped for something we could own
Bundle impact: +1.4 kb
Testability: high
Reuse: high
SSR safe: yes