useDebounce hook
typescriptA generic React hook that debounces any value by a specified delay in milliseconds, perfect for optimizing search inputs, API calls, and other performance-sensitive operations.
Code
import { useState, useEffect } from 'react';
// Generic useDebounce hook with TypeScript
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
export { useDebounce };
// Example: Search Input Component
import React, { useState, useEffect } from 'react';
interface SearchResult {
id: number;
title: string;
}
function SearchInput(): React.ReactElement {
const [searchTerm, setSearchTerm] = useState<string>('');
const [results, setResults] = useState<SearchResult[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const debouncedSearchTerm = useDebounce<string>(searchTerm, 500);
useEffect(() => {
const fetchResults = async (): Promise<void> => {
if (debouncedSearchTerm.trim() === '') {
setResults([]);
return;
}
setIsLoading(true);
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?title_like=${encodeURIComponent(debouncedSearchTerm)}`
);
const data: SearchResult[] = await response.json();
setResults(data.slice(0, 5));
} catch (error) {
console.error('Search failed:', error);
setResults([]);
} finally {
setIsLoading(false);
}
};
fetchResults();
}, [debouncedSearchTerm]);
return (
<div style={{ padding: '20px', maxWidth: '400px' }}>
<input
type="text"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
placeholder="Search posts..."
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<div style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>
{isLoading ? 'Searching...' : `Searching for: "${debouncedSearchTerm}"`}
</div>
<ul style={{ listStyle: 'none', padding: 0, marginTop: '15px' }}>
{results.map((result) => (
<li
key={result.id}
style={{
padding: '10px',
borderBottom: '1px solid #eee',
cursor: 'pointer'
}}
>
{result.title}
</li>
))}
</ul>
{!isLoading && debouncedSearchTerm && results.length === 0 && (
<p style={{ color: '#999' }}>No results found</p>
)}
</div>
);
}
export default SearchInput;How It Works
The useDebounce hook leverages React's useState and useEffect to delay updating a value until a specified amount of time has passed since the last change. When the input value changes, a timer is set. If the value changes again before the timer completes, the previous timer is cleared and a new one starts. This cleanup happens in the effect's return function, ensuring no stale updates occur.
The generic type parameter T allows this hook to work with any data type—strings, numbers, objects, or arrays. TypeScript infers the type from the initial value passed to the hook, providing full type safety throughout your component. The delay parameter is specified in milliseconds, giving you precise control over the debounce timing.
In the SearchInput example, every keystroke updates the local searchTerm state immediately for responsive UI feedback, while the debouncedSearchTerm only updates after 500ms of inactivity. This separation is crucial: the input remains snappy while expensive API calls are throttled. The effect watching debouncedSearchTerm triggers the actual search, preventing dozens of unnecessary network requests during rapid typing.
Key implementation decisions include using useState for the debounced value rather than useRef, which ensures React re-renders when the debounced value changes. The effect's dependency array includes both value and delay, so changing either will reset the debounce timer. Edge cases like empty search terms are handled by checking before making API calls.
Use this pattern for search inputs, autocomplete fields, form validation, window resize handlers, or any scenario where rapid user input triggers expensive operations. Avoid it when you need immediate feedback or when dealing with critical user actions like form submissions where delays could cause confusion. For callback debouncing rather than value debouncing, consider useCallback with a debounce wrapper instead.