The useDebouncedState hook provides debounced state management for input fields, delaying the execution of a callback until the user has stopped typing. This is ideal for search inputs, filters, and any scenario where you want to reduce API calls or expensive computations.
Import
import { useDebouncedState } from "@/hooks" ;
Signature
function useDebouncedState < T >({
initialValue ,
debounceTime ,
onChange ,
} : DebouncedStateOptions < T >) : {
value : T ;
onChangeHandler : ( event : React . ChangeEvent < HTMLInputElement >) => void ;
}
Parameters
The initial value for the state. Typically a string for text inputs.
The delay in milliseconds before calling the onChange callback after the user stops typing.
onChange
(value: T) => void
required
Callback function invoked with the debounced value after the delay period.
Return Value
The current local state value that updates immediately on input change.
onChangeHandler
(event: React.ChangeEvent<HTMLInputElement>) => void
Event handler to attach to the input’s onChange prop. Updates local state immediately and schedules the debounced onChange callback.
Usage Examples
Debounce search input to reduce API calls:
import { useDebouncedState } from "@/hooks" ;
function SearchBox () {
const { value , onChangeHandler } = useDebouncedState < string >({
initialValue: "" ,
debounceTime: 500 ,
onChange : ( searchTerm ) => {
// This runs 500ms after user stops typing
console . log ( "Searching for:" , searchTerm );
fetchSearchResults ( searchTerm );
},
});
return (
< input
type = "text"
value = { value }
onChange = { onChangeHandler }
placeholder = "Search..."
/>
);
}
Custom Debounce Time
Use a longer delay for expensive operations:
import { useDebouncedState } from "@/hooks" ;
function FilterPanel () {
const { value , onChangeHandler } = useDebouncedState < string >({
initialValue: "" ,
debounceTime: 1000 , // Wait 1 second
onChange : ( filter ) => {
// Expensive filtering operation
applyComplexFilter ( filter );
},
});
return (
< input
type = "text"
value = { value }
onChange = { onChangeHandler }
placeholder = "Filter results..."
/>
);
}
Real-World Example (DebouncedSearch Component)
From the MicroCBM codebase:
import { useDebouncedState } from "@/hooks" ;
import { Search } from "./Search" ;
interface Props {
value : string ;
onChange : ( value : string ) => void ;
debounceTime ?: number ;
}
export function DebouncedSearch ({
value ,
onChange ,
debounceTime ,
... props
} : Props ) {
const { value : innerValue , onChangeHandler } = useDebouncedState < string >({
initialValue: value ,
onChange ,
debounceTime ,
});
return < Search value = { innerValue } onChange = { onChangeHandler } { ... props } /> ;
}
With URL State Sync
Combine with useUrlState for debounced URL updates:
import { useDebouncedState } from "@/hooks" ;
import { useUrlState } from "@/hooks" ;
function SearchWithUrl () {
const [ searchParam , setSearchParam ] = useUrlState ( "q" , "" );
const { value , onChangeHandler } = useDebouncedState < string >({
initialValue: searchParam ,
debounceTime: 300 ,
onChange : ( newValue ) => {
// Update URL after user stops typing
setSearchParam ( newValue );
},
});
return (
< input
type = "text"
value = { value }
onChange = { onChangeHandler }
placeholder = "Search (synced to URL)..."
/>
);
}
API Call Example
Debounce API requests to reduce server load:
import { useDebouncedState } from "@/hooks" ;
import { useState } from "react" ;
function AssetSearch () {
const [ results , setResults ] = useState ([]);
const [ isLoading , setIsLoading ] = useState ( false );
const { value , onChangeHandler } = useDebouncedState < string >({
initialValue: "" ,
debounceTime: 500 ,
onChange : async ( query ) => {
if ( ! query ) {
setResults ([]);
return ;
}
setIsLoading ( true );
try {
const response = await fetch ( `/api/assets?search= ${ query } ` );
const data = await response . json ();
setResults ( data . assets );
} catch ( error ) {
console . error ( "Search failed:" , error );
} finally {
setIsLoading ( false );
}
},
});
return (
< div >
< input
type = "text"
value = { value }
onChange = { onChangeHandler }
placeholder = "Search assets..."
/>
{ isLoading && < div > Loading... </ div > }
< ul >
{ results . map (( asset ) => (
< li key = { asset . id } > { asset . name } </ li >
)) }
</ ul >
</ div >
);
}
Behavior
The value state updates immediately on every keystroke, providing instant visual feedback to the user.
Debounced Callback
The onChange callback is only invoked after the user stops typing for the specified debounceTime.
Timeout Management
Each new input change cancels the previous timeout and starts a new one, ensuring the callback only fires once after typing stops.
Reduced API Calls Prevent unnecessary network requests by waiting for the user to finish typing
Better UX Instant visual feedback with delayed expensive operations
Lower Server Load Fewer requests mean reduced server processing and costs
Cleaner Code Encapsulates debounce logic in a reusable hook
Notes
The value returned by this hook updates immediately. Only the onChange callback is debounced.
If the component unmounts while a debounce timeout is pending, the timeout is not automatically cleared. For cleanup, consider adding a useEffect cleanup function if needed.
Common Patterns
Typical Debounce Times
Search inputs : 300-500ms
Filters : 500-700ms
Expensive computations : 800-1000ms
Real-time validation : 200-300ms