useDebounce
The useDebounce hook delays the update of a value until a specified period of inactivity has passed. This is essential for optimizing performance in scenarios like search inputs, where you want to reduce the frequency of expensive operations such as API calls.
Type Signature
function useDebounce<T>(value: T, delay: number): T
Parameters
The value to debounce. Can be of any type (string, number, object, etc.)
The delay in milliseconds to wait before updating the debounced value
Return Value
The debounced version of the input value that only updates after the specified delay period without changes
Usage Examples
import { useDebounce } from '@hooks/useDebounce'
import { useState, useEffect } from 'react'
const SearchBar = () => {
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 300)
useEffect(() => {
if (debouncedSearch) {
// This only runs 300ms after the user stops typing
fetchSearchResults(debouncedSearch)
}
}, [debouncedSearch])
return (
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search anime..."
/>
)
}
const EmailInput = () => {
const [email, setEmail] = useState('')
const [isValid, setIsValid] = useState(false)
const debouncedEmail = useDebounce(email, 500)
useEffect(() => {
// Validate email only after user stops typing for 500ms
if (debouncedEmail) {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(debouncedEmail)
setIsValid(valid)
}
}, [debouncedEmail])
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={isValid ? 'valid' : 'invalid'}
/>
{!isValid && debouncedEmail && (
<span className="error">Invalid email format</span>
)}
</div>
)
}
Auto-Save Feature
const NoteEditor = () => {
const [content, setContent] = useState('')
const debouncedContent = useDebounce(content, 1000)
useEffect(() => {
// Auto-save 1 second after user stops typing
if (debouncedContent) {
saveNote(debouncedContent)
}
}, [debouncedContent])
return (
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Type your note..."
/>
)
}
API Call Optimization
const AnimeSearch = () => {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const { data, loading } = useFetch<Anime[]>({
url: `/api/animes/search?q=${debouncedQuery}`,
skip: !debouncedQuery,
})
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search anime..."
/>
{loading && <Spinner />}
<SearchResults results={data} />
</div>
)
}
Window Resize Handler
const ResponsiveComponent = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
})
const debouncedSize = useDebounce(windowSize, 150)
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
useEffect(() => {
// Only recalculate layout after resize stops for 150ms
recalculateLayout(debouncedSize)
}, [debouncedSize])
return <div>Window: {debouncedSize.width}x{debouncedSize.height}</div>
}
Use Cases
- Search inputs - Reduce API calls while user is typing
- Form validation - Validate fields after user finishes input
- Auto-save - Save changes after user stops editing
- Filtering - Update filtered results after filter changes stabilize
- Resize handlers - Recalculate layouts after window resize completes
- API optimization - Batch similar requests together
Without Debounce
With Debounce (300ms)
// User types "naruto" - 6 API calls
// "n" -> API call
// "na" -> API call
// "nar" -> API call
// "naru" -> API call
// "narut" -> API call
// "naruto" -> API call
// User types "naruto" - 1 API call
// User types quickly, waits 300ms
// "naruto" -> API call (only after pause)
The debounce delay should be tuned based on your use case. Common values:
- 200-300ms for search inputs
- 500-1000ms for auto-save features
- 150-200ms for resize handlers
How It Works
The hook maintains an internal debounced state and sets up a timeout effect:
- When the input
value changes, a new timeout is created
- If
value changes again before the timeout expires, the previous timeout is cleared
- Only when
value remains stable for the full delay period does the debounced value update
- The timeout is automatically cleaned up when the component unmounts
Combine useDebounce with useFetch and skip parameter for optimal search performance:const debouncedQuery = useDebounce(query, 300)
const { data } = useFetch({ url: `/search?q=${debouncedQuery}`, skip: !debouncedQuery })
Source
Location: src/domains/search/stores/useDebounce.ts:25