The hook stores the latest callback in a ref so the debounce timer always calls the newest version without needing to reset itself. The delay defaults to 300ms.
typescript
import { useCallback, useEffect, useRef } from 'react'
export function useDebouncedCallback<Args extends unknown[]>(
callback: (...args: Args) => void,
delay = 300,
) {
const callbackRef = useRef(callback)
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
useEffect(() => {
callbackRef.current = callback
}, [callback])
useEffect(() => {
return () => clearTimeout(timeoutRef.current)
}, [])
return useCallback(
(...args: Args) => {
clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => callbackRef.current(...args), delay)
},
[delay],
)
}In a search input, wire the debounced function to onChange. The input stays fully controlled and responsive; only the search request itself is delayed.
typescript
function SearchBox({ onSearch }: { onSearch: (query: string) => void }) {
const [value, setValue] = useState('')
const debouncedSearch = useDebouncedCallback(onSearch, 300)
return (
<input
value={value}
onChange={(e) => {
setValue(e.target.value)
debouncedSearch(e.target.value)
}}
placeholder="Search..."
/>
)
}