0
# Callback Debouncing
1
2
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.
3
4
## Capabilities
5
6
### useDebouncedCallback Hook
7
8
Creates a debounced version of a callback function with comprehensive control options.
9
10
```typescript { .api }
11
/**
12
* Creates a debounced function that delays invoking func until after wait
13
* milliseconds have elapsed since the last time the debounced function was
14
* invoked, or until the next browser frame is drawn.
15
*
16
* @param func - The function to debounce
17
* @param wait - The number of milliseconds to delay (defaults to requestAnimationFrame if 0 or omitted)
18
* @param options - Optional configuration object
19
* @returns Debounced function with control methods
20
*/
21
function useDebouncedCallback<T extends (...args: any) => ReturnType<T>>(
22
func: T,
23
wait?: number,
24
options?: Options
25
): DebouncedState<T>;
26
```
27
28
**Parameters:**
29
30
- `func: T` - The function to debounce
31
- `wait?: number` - Wait time in milliseconds. If 0 or omitted, uses `requestAnimationFrame` in browser environments
32
- `options?: Options` - Configuration object with:
33
- `leading?: boolean` - If true, invokes the function on the leading edge of the timeout (default: false)
34
- `trailing?: boolean` - If true, invokes the function on the trailing edge of the timeout (default: true)
35
- `maxWait?: number` - Maximum time the function is allowed to be delayed before it's invoked
36
- `debounceOnServer?: boolean` - If true, enables debouncing in server-side environments (default: false)
37
38
**Returns:**
39
- `DebouncedState<T>` - Debounced function with control methods (`cancel`, `flush`, `isPending`)
40
41
**Usage Examples:**
42
43
```typescript
44
import React, { useState } from 'react';
45
import { useDebouncedCallback } from 'use-debounce';
46
47
// Basic API call debouncing
48
function SearchComponent() {
49
const [results, setResults] = useState([]);
50
const [loading, setLoading] = useState(false);
51
52
const debouncedSearch = useDebouncedCallback(
53
async (searchTerm: string) => {
54
if (!searchTerm.trim()) return;
55
56
setLoading(true);
57
try {
58
const response = await fetch(`/api/search?q=${searchTerm}`);
59
const data = await response.json();
60
setResults(data.results);
61
} catch (error) {
62
console.error('Search failed:', error);
63
} finally {
64
setLoading(false);
65
}
66
},
67
500
68
);
69
70
return (
71
<div>
72
<input
73
type="text"
74
onChange={(e) => debouncedSearch(e.target.value)}
75
placeholder="Search..."
76
/>
77
{loading && <p>Searching...</p>}
78
<ul>
79
{results.map((result) => (
80
<li key={result.id}>{result.title}</li>
81
))}
82
</ul>
83
</div>
84
);
85
}
86
87
// Leading edge execution
88
function QuickAction() {
89
const debouncedAction = useDebouncedCallback(
90
(action: string) => {
91
console.log('Executing:', action);
92
// Perform action immediately on first call
93
},
94
1000,
95
{ leading: true, trailing: false }
96
);
97
98
return (
99
<button onClick={() => debouncedAction('quick-save')}>
100
Quick Save
101
</button>
102
);
103
}
104
105
// With maxWait option
106
function AutoSave() {
107
const [content, setContent] = useState('');
108
109
const debouncedSave = useDebouncedCallback(
110
async (data: string) => {
111
console.log('Auto-saving...', data);
112
await fetch('/api/save', {
113
method: 'POST',
114
body: JSON.stringify({ content: data }),
115
headers: { 'Content-Type': 'application/json' }
116
});
117
},
118
2000,
119
{ maxWait: 10000 } // Force save every 10 seconds maximum
120
);
121
122
React.useEffect(() => {
123
debouncedSave(content);
124
}, [content, debouncedSave]);
125
126
return (
127
<textarea
128
value={content}
129
onChange={(e) => setContent(e.target.value)}
130
placeholder="Type here... (auto-saves)"
131
/>
132
);
133
}
134
135
// Event handling with native listeners
136
function ScrollHandler() {
137
const [position, setPosition] = useState(0);
138
139
const debouncedScrollHandler = useDebouncedCallback(
140
() => {
141
setPosition(window.pageYOffset);
142
},
143
100
144
);
145
146
React.useEffect(() => {
147
window.addEventListener('scroll', debouncedScrollHandler);
148
return () => {
149
window.removeEventListener('scroll', debouncedScrollHandler);
150
};
151
}, [debouncedScrollHandler]);
152
153
return <div>Scroll position: {position}px</div>;
154
}
155
156
// Using control functions
157
function ControlledCallback() {
158
const [message, setMessage] = useState('');
159
160
const debouncedSubmit = useDebouncedCallback(
161
(msg: string) => {
162
console.log('Submitting:', msg);
163
// Submit message
164
},
165
1000
166
);
167
168
const handleSubmit = () => {
169
debouncedSubmit(message);
170
};
171
172
const handleCancel = () => {
173
debouncedSubmit.cancel();
174
};
175
176
const handleFlush = () => {
177
debouncedSubmit.flush();
178
};
179
180
return (
181
<div>
182
<input
183
value={message}
184
onChange={(e) => setMessage(e.target.value)}
185
/>
186
<button onClick={handleSubmit}>Submit (Debounced)</button>
187
<button onClick={handleCancel}>Cancel</button>
188
<button onClick={handleFlush}>Submit Now</button>
189
<p>Pending: {debouncedSubmit.isPending() ? 'Yes' : 'No'}</p>
190
</div>
191
);
192
}
193
194
// Form validation
195
function ValidatedForm() {
196
const [form, setForm] = useState({ email: '', password: '' });
197
const [errors, setErrors] = useState({});
198
199
const debouncedValidate = useDebouncedCallback(
200
(formData: typeof form) => {
201
const newErrors: any = {};
202
203
if (!formData.email.includes('@')) {
204
newErrors.email = 'Invalid email';
205
}
206
207
if (formData.password.length < 8) {
208
newErrors.password = 'Password too short';
209
}
210
211
setErrors(newErrors);
212
},
213
300
214
);
215
216
React.useEffect(() => {
217
debouncedValidate(form);
218
}, [form, debouncedValidate]);
219
220
return (
221
<form>
222
<input
223
type="email"
224
value={form.email}
225
onChange={(e) => setForm(prev => ({ ...prev, email: e.target.value }))}
226
/>
227
{errors.email && <span>{errors.email}</span>}
228
229
<input
230
type="password"
231
value={form.password}
232
onChange={(e) => setForm(prev => ({ ...prev, password: e.target.value }))}
233
/>
234
{errors.password && <span>{errors.password}</span>}
235
</form>
236
);
237
}
238
```
239
240
### Return Value Handling
241
242
The debounced function returns the result of the last invocation. If there are no previous invocations, it returns `undefined`.
243
244
```typescript
245
function ReturnValueExample() {
246
const debouncedCalculate = useDebouncedCallback(
247
(a: number, b: number) => {
248
return a + b;
249
},
250
500
251
);
252
253
const handleCalculate = () => {
254
// First call returns undefined (no previous invocation)
255
const result = debouncedCalculate(5, 3);
256
console.log('Immediate result:', result); // undefined
257
258
// After the debounce period, subsequent calls return the last result
259
setTimeout(() => {
260
const cachedResult = debouncedCalculate(10, 7);
261
console.log('Cached result:', cachedResult); // 8 (from previous call)
262
}, 1000);
263
};
264
265
return <button onClick={handleCalculate}>Calculate</button>;
266
}
267
```
268
269
### Server-Side Rendering
270
271
By default, debouncing is disabled in server-side environments. Enable it with the `debounceOnServer` option:
272
273
```typescript
274
function SSRCompatible() {
275
const debouncedCallback = useDebouncedCallback(
276
(data: string) => {
277
console.log('Processing:', data);
278
},
279
500,
280
{ debounceOnServer: true }
281
);
282
283
// This will work in both client and server environments
284
return (
285
<button onClick={() => debouncedCallback('data')}>
286
Process
287
</button>
288
);
289
}
290
```
291
292
## Options Interface
293
294
```typescript { .api }
295
interface Options extends CallOptions {
296
/** The maximum time the given function is allowed to be delayed before it's invoked */
297
maxWait?: number;
298
/** If set to true, all debouncing and timers will happen on the server side as well */
299
debounceOnServer?: boolean;
300
}
301
302
interface CallOptions {
303
/** Controls if the function should be invoked on the leading edge of the timeout */
304
leading?: boolean;
305
/** Controls if the function should be invoked on the trailing edge of the timeout */
306
trailing?: boolean;
307
}
308
```
309
310
## Common Patterns
311
312
### API Rate Limiting
313
314
```typescript
315
function RateLimitedAPI() {
316
const debouncedRequest = useDebouncedCallback(
317
async (endpoint: string, data: any) => {
318
return fetch(endpoint, {
319
method: 'POST',
320
body: JSON.stringify(data),
321
headers: { 'Content-Type': 'application/json' }
322
});
323
},
324
1000 // Limit to one request per second
325
);
326
327
return {
328
createUser: (userData: any) => debouncedRequest('/api/users', userData),
329
updateUser: (userId: string, userData: any) =>
330
debouncedRequest(`/api/users/${userId}`, userData)
331
};
332
}
333
```
334
335
### Expensive Calculations
336
337
```typescript
338
function ExpensiveCalculation() {
339
const [input, setInput] = useState('');
340
const [result, setResult] = useState('');
341
342
const debouncedCalculate = useDebouncedCallback(
343
(value: string) => {
344
// Simulate expensive calculation
345
const processed = value
346
.split('')
347
.reverse()
348
.map(char => char.charCodeAt(0))
349
.reduce((sum, code) => sum + code, 0);
350
351
setResult(`Calculated: ${processed}`);
352
},
353
500
354
);
355
356
React.useEffect(() => {
357
if (input) {
358
debouncedCalculate(input);
359
}
360
}, [input, debouncedCalculate]);
361
362
return (
363
<div>
364
<input value={input} onChange={(e) => setInput(e.target.value)} />
365
<p>{result}</p>
366
</div>
367
);
368
}
369
```
370
371
### Cleanup on Unmount
372
373
```typescript
374
function ComponentWithCleanup() {
375
const debouncedSave = useDebouncedCallback(
376
(data: string) => {
377
// Save data
378
console.log('Saving:', data);
379
},
380
1000
381
);
382
383
React.useEffect(() => {
384
// Cleanup: flush any pending saves when component unmounts
385
return () => {
386
debouncedSave.flush();
387
};
388
}, [debouncedSave]);
389
390
return (
391
<input onChange={(e) => debouncedSave(e.target.value)} />
392
);
393
}
394
```