React hooks library for debouncing and throttling functionality with small footprint and comprehensive control features
—
Callback debouncing with useDebouncedCallback creates debounced versions of functions that delay execution until after a specified wait time has passed since the last invocation. This is ideal for optimizing expensive operations like API calls, search queries, form submissions, and event handlers.
Creates a debounced version of a callback function with comprehensive control options.
/**
* Creates a debounced function that delays invoking func until after wait
* milliseconds have elapsed since the last time the debounced function was
* invoked, or until the next browser frame is drawn.
*
* @param func - The function to debounce
* @param wait - The number of milliseconds to delay (defaults to requestAnimationFrame if 0 or omitted)
* @param options - Optional configuration object
* @returns Debounced function with control methods
*/
function useDebouncedCallback<T extends (...args: any) => ReturnType<T>>(
func: T,
wait?: number,
options?: Options
): DebouncedState<T>;Parameters:
func: T - The function to debouncewait?: number - Wait time in milliseconds. If 0 or omitted, uses requestAnimationFrame in browser environmentsoptions?: Options - Configuration object with:
leading?: boolean - If true, invokes the function on the leading edge of the timeout (default: false)trailing?: boolean - If true, invokes the function on the trailing edge of the timeout (default: true)maxWait?: number - Maximum time the function is allowed to be delayed before it's invokeddebounceOnServer?: boolean - If true, enables debouncing in server-side environments (default: false)Returns:
DebouncedState<T> - Debounced function with control methods (cancel, flush, isPending)Usage Examples:
import React, { useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
// Basic API call debouncing
function SearchComponent() {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedSearch = useDebouncedCallback(
async (searchTerm: string) => {
if (!searchTerm.trim()) return;
setLoading(true);
try {
const response = await fetch(`/api/search?q=${searchTerm}`);
const data = await response.json();
setResults(data.results);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
},
500
);
return (
<div>
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
{loading && <p>Searching...</p>}
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
// Leading edge execution
function QuickAction() {
const debouncedAction = useDebouncedCallback(
(action: string) => {
console.log('Executing:', action);
// Perform action immediately on first call
},
1000,
{ leading: true, trailing: false }
);
return (
<button onClick={() => debouncedAction('quick-save')}>
Quick Save
</button>
);
}
// With maxWait option
function AutoSave() {
const [content, setContent] = useState('');
const debouncedSave = useDebouncedCallback(
async (data: string) => {
console.log('Auto-saving...', data);
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ content: data }),
headers: { 'Content-Type': 'application/json' }
});
},
2000,
{ maxWait: 10000 } // Force save every 10 seconds maximum
);
React.useEffect(() => {
debouncedSave(content);
}, [content, debouncedSave]);
return (
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Type here... (auto-saves)"
/>
);
}
// Event handling with native listeners
function ScrollHandler() {
const [position, setPosition] = useState(0);
const debouncedScrollHandler = useDebouncedCallback(
() => {
setPosition(window.pageYOffset);
},
100
);
React.useEffect(() => {
window.addEventListener('scroll', debouncedScrollHandler);
return () => {
window.removeEventListener('scroll', debouncedScrollHandler);
};
}, [debouncedScrollHandler]);
return <div>Scroll position: {position}px</div>;
}
// Using control functions
function ControlledCallback() {
const [message, setMessage] = useState('');
const debouncedSubmit = useDebouncedCallback(
(msg: string) => {
console.log('Submitting:', msg);
// Submit message
},
1000
);
const handleSubmit = () => {
debouncedSubmit(message);
};
const handleCancel = () => {
debouncedSubmit.cancel();
};
const handleFlush = () => {
debouncedSubmit.flush();
};
return (
<div>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={handleSubmit}>Submit (Debounced)</button>
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleFlush}>Submit Now</button>
<p>Pending: {debouncedSubmit.isPending() ? 'Yes' : 'No'}</p>
</div>
);
}
// Form validation
function ValidatedForm() {
const [form, setForm] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const debouncedValidate = useDebouncedCallback(
(formData: typeof form) => {
const newErrors: any = {};
if (!formData.email.includes('@')) {
newErrors.email = 'Invalid email';
}
if (formData.password.length < 8) {
newErrors.password = 'Password too short';
}
setErrors(newErrors);
},
300
);
React.useEffect(() => {
debouncedValidate(form);
}, [form, debouncedValidate]);
return (
<form>
<input
type="email"
value={form.email}
onChange={(e) => setForm(prev => ({ ...prev, email: e.target.value }))}
/>
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={form.password}
onChange={(e) => setForm(prev => ({ ...prev, password: e.target.value }))}
/>
{errors.password && <span>{errors.password}</span>}
</form>
);
}The debounced function returns the result of the last invocation. If there are no previous invocations, it returns undefined.
function ReturnValueExample() {
const debouncedCalculate = useDebouncedCallback(
(a: number, b: number) => {
return a + b;
},
500
);
const handleCalculate = () => {
// First call returns undefined (no previous invocation)
const result = debouncedCalculate(5, 3);
console.log('Immediate result:', result); // undefined
// After the debounce period, subsequent calls return the last result
setTimeout(() => {
const cachedResult = debouncedCalculate(10, 7);
console.log('Cached result:', cachedResult); // 8 (from previous call)
}, 1000);
};
return <button onClick={handleCalculate}>Calculate</button>;
}By default, debouncing is disabled in server-side environments. Enable it with the debounceOnServer option:
function SSRCompatible() {
const debouncedCallback = useDebouncedCallback(
(data: string) => {
console.log('Processing:', data);
},
500,
{ debounceOnServer: true }
);
// This will work in both client and server environments
return (
<button onClick={() => debouncedCallback('data')}>
Process
</button>
);
}interface Options extends CallOptions {
/** The maximum time the given function is allowed to be delayed before it's invoked */
maxWait?: number;
/** If set to true, all debouncing and timers will happen on the server side as well */
debounceOnServer?: boolean;
}
interface CallOptions {
/** Controls if the function should be invoked on the leading edge of the timeout */
leading?: boolean;
/** Controls if the function should be invoked on the trailing edge of the timeout */
trailing?: boolean;
}function RateLimitedAPI() {
const debouncedRequest = useDebouncedCallback(
async (endpoint: string, data: any) => {
return fetch(endpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
});
},
1000 // Limit to one request per second
);
return {
createUser: (userData: any) => debouncedRequest('/api/users', userData),
updateUser: (userId: string, userData: any) =>
debouncedRequest(`/api/users/${userId}`, userData)
};
}function ExpensiveCalculation() {
const [input, setInput] = useState('');
const [result, setResult] = useState('');
const debouncedCalculate = useDebouncedCallback(
(value: string) => {
// Simulate expensive calculation
const processed = value
.split('')
.reverse()
.map(char => char.charCodeAt(0))
.reduce((sum, code) => sum + code, 0);
setResult(`Calculated: ${processed}`);
},
500
);
React.useEffect(() => {
if (input) {
debouncedCalculate(input);
}
}, [input, debouncedCalculate]);
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<p>{result}</p>
</div>
);
}function ComponentWithCleanup() {
const debouncedSave = useDebouncedCallback(
(data: string) => {
// Save data
console.log('Saving:', data);
},
1000
);
React.useEffect(() => {
// Cleanup: flush any pending saves when component unmounts
return () => {
debouncedSave.flush();
};
}, [debouncedSave]);
return (
<input onChange={(e) => debouncedSave(e.target.value)} />
);
}Install with Tessl CLI
npx tessl i tessl/npm-use-debounce