The useDebounce hook provides a simple way to debounce rapidly changing values, useful for optimizing performance in search inputs, API calls, and expensive computations.
Signature
useDebounce < T >( state : T , time ?: number ): T
Parameters
The value to debounce. Can be any type (string, number, object, array, etc.)
Delay in milliseconds before updating the debounced value. Defaults to 300ms.
Returns
The debounced version of the input state. Updates only after the specified delay period has passed without changes to the input.
Usage Example
import { useState } from 'react'
import { useDebounce } from '@/hooks/useDebounce'
function SearchBar () {
const [ searchTerm , setSearchTerm ] = useState ( '' )
const debouncedSearch = useDebounce ( searchTerm , 500 )
// This effect runs only when user stops typing for 500ms
useEffect (() => {
if ( debouncedSearch ) {
console . log ( 'Searching for:' , debouncedSearch )
// Make API call or filter data
fetchSearchResults ( debouncedSearch )
}
}, [ debouncedSearch ])
return (
< input
type = "text"
value = { searchTerm }
onChange = {(e) => setSearchTerm (e.target.value)}
placeholder = "Search Pokemon..."
/>
)
}
Behavior :
User types “p” → No API call yet
User types “pi” → No API call yet
User types “pik” → No API call yet
User stops typing for 500ms → API call with “pik”
User types “pika” → Previous timeout canceled, new timeout starts
User stops typing for 500ms → API call with “pika”
Advanced Examples
function EmailInput () {
const [ email , setEmail ] = useState ( '' )
const debouncedEmail = useDebounce ( email , 1000 )
const [ isValid , setIsValid ] = useState < boolean | null >( null )
useEffect (() => {
if ( debouncedEmail ) {
// Validate only after user stops typing
const valid = / ^ [ ^ \s@ ] + @ [ ^ \s@ ] + \. [ ^ \s@ ] + $ / . test ( debouncedEmail )
setIsValid ( valid )
}
}, [ debouncedEmail ])
return (
< div >
< input
type = "email"
value = { email }
onChange = {(e) => setEmail (e.target.value)}
/>
{ isValid === false && < span > Invalid email </ span >}
{ isValid === true && < span > Valid email </ span >}
</ div >
)
}
Debounced Window Resize
function ResponsiveComponent () {
const [ windowWidth , setWindowWidth ] = useState ( window . innerWidth )
const debouncedWidth = useDebounce ( windowWidth , 200 )
useEffect (() => {
const handleResize = () => setWindowWidth ( window . innerWidth )
window . addEventListener ( 'resize' , handleResize )
return () => window . removeEventListener ( 'resize' , handleResize )
}, [])
useEffect (() => {
// Expensive layout recalculation only after resize stops
console . log ( 'Recalculating layout for width:' , debouncedWidth )
recalculateLayout ()
}, [ debouncedWidth ])
return < div > Window width : { debouncedWidth } px </ div >
}
Debounced API Call with Loading State
function PokemonSearch () {
const [ query , setQuery ] = useState ( '' )
const [ results , setResults ] = useState ([])
const [ loading , setLoading ] = useState ( false )
const debouncedQuery = useDebounce ( query , 400 )
useEffect (() => {
if ( ! debouncedQuery ) {
setResults ([])
return
}
setLoading ( true )
searchPokemon ( debouncedQuery )
. then ( data => setResults ( data ))
. finally (() => setLoading ( false ))
}, [ debouncedQuery ])
return (
< div >
< input
value = { query }
onChange = {(e) => setQuery (e.target.value)}
/>
{ loading && < span > Searching ...</ span >}
{ results . map ( pokemon => < div key ={ pokemon . id }>{pokemon. name } </ div > )}
</ div >
)
}
How It Works
The hook uses setTimeout to delay updates:
useEffect (() => {
const timeoutID = setTimeout (() => {
setDebState ( state )
}, time )
return () => {
clearTimeout ( timeoutID )
}
}, [ state , time ])
Step-by-step :
Input changes → state parameter updates
Effect runs → Previous timeout is cleared (cleanup)
New timeout starts → Waits for time milliseconds
If input changes again → Repeat from step 2
If timeout completes → debouncedValue updates
Without Debounce
// API call on every keystroke
function BadSearch () {
const [ query , setQuery ] = useState ( '' )
useEffect (() => {
fetchResults ( query ) // Called for EVERY character typed
}, [ query ])
return < input onChange = { e => setQuery ( e . target . value )} />
}
// User types "pikachu" ( 7 characters ) = 7 API calls !
With Debounce
// API call only after user stops typing
function GoodSearch () {
const [ query , setQuery ] = useState ( '' )
const debouncedQuery = useDebounce ( query , 300 )
useEffect (() => {
fetchResults ( debouncedQuery ) // Called once after typing stops
}, [ debouncedQuery ])
return < input onChange = { e => setQuery ( e . target . value )} />
}
// User types "pikachu" = 1 API call
Savings : 85% fewer API calls in this example
Choosing the Right Delay
100-200ms Use for : Auto-save, real-time validation
UX : Feels almost instant, minimal delay
Trade-off : Still processes frequently
300-500ms Use for : Search inputs, filters
UX : Balanced responsiveness
Trade-off : Standard choice for most use cases
500-1000ms Use for : Expensive operations, API rate limits
UX : Noticeable delay but acceptable
Trade-off : Reduces server load significantly
1000ms+ Use for : Complex calculations, analytics
UX : User must pause deliberately
Trade-off : Maximum performance savings
Common Patterns
function Search () {
const [ query , setQuery ] = useState ( '' )
const debouncedQuery = useDebounce ( query , 300 )
const clear = () => setQuery ( '' )
return (
< div >
< input
value = { query }
onChange = { e => setQuery ( e . target . value )}
/>
{ query && < button onClick = { clear } > Clear </ button > }
< Results query = { debouncedQuery } />
</ div >
)
}
Pattern 2: Debounce with Minimum Length
function SmartSearch () {
const [ query , setQuery ] = useState ( '' )
const debouncedQuery = useDebounce ( query , 400 )
useEffect (() => {
// Only search if at least 3 characters
if ( debouncedQuery . length >= 3 ) {
performSearch ( debouncedQuery )
}
}, [ debouncedQuery ])
return < input onChange = { e => setQuery ( e . target . value )} />
}
Pattern 3: Debounce Multiple Values
function MultiFilter () {
const [ name , setName ] = useState ( '' )
const [ type , setType ] = useState ( '' )
const [ region , setRegion ] = useState ( '' )
const debouncedName = useDebounce ( name , 300 )
const debouncedType = useDebounce ( type , 300 )
const debouncedRegion = useDebounce ( region , 300 )
useEffect (() => {
// Filters only update after all inputs are debounced
applyFilters ({
name: debouncedName ,
type: debouncedType ,
region: debouncedRegion
})
}, [ debouncedName , debouncedType , debouncedRegion ])
return (
< div >
< input onChange = { e => setName ( e . target . value )} />
< input onChange = { e => setType ( e . target . value )} />
< input onChange = { e => setRegion ( e . target . value )} />
</ div >
)
}
TypeScript Usage
The hook is fully generic and type-safe:
// String
const debouncedString = useDebounce < string >( 'hello' , 300 )
// Number
const debouncedNumber = useDebounce < number >( 42 , 500 )
// Object
interface Filter {
name : string
types : string []
}
const debouncedFilter = useDebounce < Filter >(
{ name: 'pikachu' , types: [ 'electric' ] },
400
)
// Array
const debouncedArray = useDebounce < number []>([ 1 , 2 , 3 ], 200 )
// Type inference works automatically
const debouncedAuto = useDebounce ( 'auto' , 300 ) // Type is string