0
# Throttling
1
2
Throttling with `useThrottledCallback` creates functions that execute at most once per specified interval. Unlike debouncing, which delays execution until activity stops, throttling ensures regular execution during continuous activity. This is ideal for high-frequency events like scrolling, resizing, mouse movement, or animation frames.
3
4
## Capabilities
5
6
### useThrottledCallback Hook
7
8
Creates a throttled version of a callback function that executes at most once per wait interval.
9
10
```typescript { .api }
11
/**
12
* Creates a throttled function that only invokes func at most once per
13
* every wait milliseconds (or once per browser frame).
14
*
15
* @param func - The function to throttle
16
* @param wait - The number of milliseconds to throttle invocations to
17
* @param options - Optional configuration for leading/trailing execution
18
* @returns Throttled function with control methods
19
*/
20
function useThrottledCallback<T extends (...args: any) => ReturnType<T>>(
21
func: T,
22
wait: number,
23
options?: CallOptions
24
): DebouncedState<T>;
25
```
26
27
**Parameters:**
28
29
- `func: T` - The function to throttle
30
- `wait: number` - The throttle interval in milliseconds
31
- `options?: CallOptions` - Configuration object with:
32
- `leading?: boolean` - If true, invokes the function on the leading edge (default: true)
33
- `trailing?: boolean` - If true, invokes the function on the trailing edge (default: true)
34
35
**Returns:**
36
- `DebouncedState<T>` - Throttled function with control methods (`cancel`, `flush`, `isPending`)
37
38
**Usage Examples:**
39
40
```typescript
41
import React, { useState, useEffect } from 'react';
42
import { useThrottledCallback } from 'use-debounce';
43
44
// Scroll position tracking
45
function ScrollTracker() {
46
const [scrollY, setScrollY] = useState(0);
47
48
const throttledScrollHandler = useThrottledCallback(
49
() => {
50
setScrollY(window.pageYOffset);
51
},
52
100 // Update scroll position at most once every 100ms
53
);
54
55
useEffect(() => {
56
window.addEventListener('scroll', throttledScrollHandler);
57
return () => {
58
window.removeEventListener('scroll', throttledScrollHandler);
59
};
60
}, [throttledScrollHandler]);
61
62
return (
63
<div style={{ position: 'fixed', top: 0, left: 0, background: 'white' }}>
64
Scroll Y: {scrollY}px
65
</div>
66
);
67
}
68
69
// Window resize handling
70
function ResponsiveComponent() {
71
const [windowSize, setWindowSize] = useState({
72
width: typeof window !== 'undefined' ? window.innerWidth : 0,
73
height: typeof window !== 'undefined' ? window.innerHeight : 0,
74
});
75
76
const throttledResizeHandler = useThrottledCallback(
77
() => {
78
setWindowSize({
79
width: window.innerWidth,
80
height: window.innerHeight,
81
});
82
},
83
200 // Recalculate layout at most once every 200ms
84
);
85
86
useEffect(() => {
87
window.addEventListener('resize', throttledResizeHandler);
88
return () => {
89
window.removeEventListener('resize', throttledResizeHandler);
90
};
91
}, [throttledResizeHandler]);
92
93
return (
94
<div>
95
Window size: {windowSize.width} x {windowSize.height}
96
</div>
97
);
98
}
99
100
// Mouse movement tracking
101
function MouseTracker() {
102
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
103
104
const throttledMouseMove = useThrottledCallback(
105
(event: MouseEvent) => {
106
setMousePos({ x: event.clientX, y: event.clientY });
107
},
108
50 // Update mouse position at most once every 50ms
109
);
110
111
useEffect(() => {
112
const handleMouseMove = (e: MouseEvent) => throttledMouseMove(e);
113
document.addEventListener('mousemove', handleMouseMove);
114
return () => {
115
document.removeEventListener('mousemove', handleMouseMove);
116
};
117
}, [throttledMouseMove]);
118
119
return (
120
<div>
121
Mouse: ({mousePos.x}, {mousePos.y})
122
</div>
123
);
124
}
125
126
// API requests with rate limiting
127
function RateLimitedRequests() {
128
const [data, setData] = useState(null);
129
const [requestCount, setRequestCount] = useState(0);
130
131
const throttledFetch = useThrottledCallback(
132
async (endpoint: string) => {
133
setRequestCount(prev => prev + 1);
134
const response = await fetch(endpoint);
135
const result = await response.json();
136
setData(result);
137
},
138
5000 // Maximum one request every 5 seconds
139
);
140
141
return (
142
<div>
143
<button onClick={() => throttledFetch('/api/data')}>
144
Fetch Data (Throttled)
145
</button>
146
<p>Requests made: {requestCount}</p>
147
<pre>{JSON.stringify(data, null, 2)}</pre>
148
</div>
149
);
150
}
151
152
// Leading edge only (immediate execution, then throttled)
153
function ImmediateAction() {
154
const [count, setCount] = useState(0);
155
156
const throttledIncrement = useThrottledCallback(
157
() => {
158
setCount(prev => prev + 1);
159
},
160
1000,
161
{ leading: true, trailing: false }
162
);
163
164
return (
165
<div>
166
<button onClick={throttledIncrement}>
167
Increment (Immediate, then throttled)
168
</button>
169
<p>Count: {count}</p>
170
</div>
171
);
172
}
173
174
// Trailing edge only (throttled execution only)
175
function TrailingAction() {
176
const [lastAction, setLastAction] = useState('');
177
178
const throttledLog = useThrottledCallback(
179
(action: string) => {
180
setLastAction(`${action} at ${new Date().toLocaleTimeString()}`);
181
},
182
2000,
183
{ leading: false, trailing: true }
184
);
185
186
return (
187
<div>
188
<button onClick={() => throttledLog('Button clicked')}>
189
Click Me (Trailing only)
190
</button>
191
<p>Last action: {lastAction}</p>
192
</div>
193
);
194
}
195
196
// Animation frame throttling
197
function AnimationExample() {
198
const [rotation, setRotation] = useState(0);
199
200
const throttledRotate = useThrottledCallback(
201
() => {
202
setRotation(prev => (prev + 10) % 360);
203
},
204
16 // ~60fps throttling
205
);
206
207
useEffect(() => {
208
const interval = setInterval(throttledRotate, 10);
209
return () => clearInterval(interval);
210
}, [throttledRotate]);
211
212
return (
213
<div
214
style={{
215
width: 100,
216
height: 100,
217
background: 'blue',
218
transform: `rotate(${rotation}deg)`,
219
transition: 'transform 0.1s ease',
220
}}
221
/>
222
);
223
}
224
225
// Search with throttled API calls
226
function ThrottledSearch() {
227
const [query, setQuery] = useState('');
228
const [results, setResults] = useState([]);
229
const [isSearching, setIsSearching] = useState(false);
230
231
const throttledSearch = useThrottledCallback(
232
async (searchTerm: string) => {
233
if (!searchTerm.trim()) {
234
setResults([]);
235
return;
236
}
237
238
setIsSearching(true);
239
try {
240
const response = await fetch(`/api/search?q=${searchTerm}`);
241
const data = await response.json();
242
setResults(data.results);
243
} catch (error) {
244
console.error('Search failed:', error);
245
} finally {
246
setIsSearching(false);
247
}
248
},
249
800 // Maximum one search every 800ms
250
);
251
252
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
253
const value = e.target.value;
254
setQuery(value);
255
throttledSearch(value);
256
};
257
258
return (
259
<div>
260
<input
261
type="text"
262
value={query}
263
onChange={handleInputChange}
264
placeholder="Search (throttled)..."
265
/>
266
{isSearching && <p>Searching...</p>}
267
<ul>
268
{results.map((result: any) => (
269
<li key={result.id}>{result.title}</li>
270
))}
271
</ul>
272
</div>
273
);
274
}
275
```
276
277
### Control Functions Usage
278
279
```typescript
280
function ThrottleWithControls() {
281
const [value, setValue] = useState(0);
282
283
const throttledUpdate = useThrottledCallback(
284
(newValue: number) => {
285
setValue(newValue);
286
},
287
1000
288
);
289
290
return (
291
<div>
292
<button onClick={() => throttledUpdate(value + 1)}>
293
Increment (Throttled)
294
</button>
295
<button onClick={() => throttledUpdate.cancel()}>
296
Cancel Pending
297
</button>
298
<button onClick={() => throttledUpdate.flush()}>
299
Execute Now
300
</button>
301
<p>Value: {value}</p>
302
<p>Has Pending: {throttledUpdate.isPending() ? 'Yes' : 'No'}</p>
303
</div>
304
);
305
}
306
```
307
308
## CallOptions Interface
309
310
```typescript { .api }
311
interface CallOptions {
312
/**
313
* Specify invoking on the leading edge of the timeout
314
* Default: true for useThrottledCallback
315
*/
316
leading?: boolean;
317
/**
318
* Specify invoking on the trailing edge of the timeout
319
* Default: true for useThrottledCallback
320
*/
321
trailing?: boolean;
322
}
323
```
324
325
## Throttling vs Debouncing
326
327
**Throttling** ensures a function executes at regular intervals during continuous activity:
328
- **Use case**: Scroll handlers, resize handlers, mouse movement, animation
329
- **Behavior**: Executes immediately, then at most once per interval
330
- **Example**: Update scroll position every 100ms while scrolling
331
332
**Debouncing** delays execution until activity stops:
333
- **Use case**: Search input, form validation, API calls
334
- **Behavior**: Waits for quiet period before executing
335
- **Example**: Search API call 500ms after user stops typing
336
337
```typescript
338
// Throttling - executes regularly during scrolling
339
const throttledScroll = useThrottledCallback(
340
() => console.log('Scroll position:', window.scrollY),
341
100 // Every 100ms while scrolling
342
);
343
344
// Debouncing - executes once after scrolling stops
345
const debouncedScroll = useDebouncedCallback(
346
() => console.log('Scroll ended at:', window.scrollY),
347
100 // 100ms after scrolling stops
348
);
349
```
350
351
## Common Patterns
352
353
### Performance Monitoring
354
355
```typescript
356
function PerformanceMonitor() {
357
const [fps, setFPS] = useState(0);
358
const frameCount = useRef(0);
359
const lastTime = useRef(performance.now());
360
361
const throttledFPSUpdate = useThrottledCallback(
362
() => {
363
const now = performance.now();
364
const delta = now - lastTime.current;
365
const currentFPS = Math.round((frameCount.current * 1000) / delta);
366
367
setFPS(currentFPS);
368
frameCount.current = 0;
369
lastTime.current = now;
370
},
371
1000 // Update FPS display once per second
372
);
373
374
useEffect(() => {
375
const animate = () => {
376
frameCount.current++;
377
throttledFPSUpdate();
378
requestAnimationFrame(animate);
379
};
380
381
const id = requestAnimationFrame(animate);
382
return () => cancelAnimationFrame(id);
383
}, [throttledFPSUpdate]);
384
385
return <div>FPS: {fps}</div>;
386
}
387
```
388
389
### Event Delegation with Throttling
390
391
```typescript
392
function ThrottledEventDelegation() {
393
const throttledHandler = useThrottledCallback(
394
(event: Event) => {
395
const target = event.target as HTMLElement;
396
if (target.matches('.interactive')) {
397
console.log('Interacted with:', target.textContent);
398
}
399
},
400
200
401
);
402
403
useEffect(() => {
404
document.addEventListener('click', throttledHandler);
405
return () => {
406
document.removeEventListener('click', throttledHandler);
407
};
408
}, [throttledHandler]);
409
410
return (
411
<div>
412
<div className="interactive">Button 1</div>
413
<div className="interactive">Button 2</div>
414
<div className="interactive">Button 3</div>
415
</div>
416
);
417
}
418
```
419
420
### Progressive Loading
421
422
```typescript
423
function ProgressiveLoader() {
424
const [items, setItems] = useState([]);
425
const [page, setPage] = useState(1);
426
427
const throttledLoadMore = useThrottledCallback(
428
async () => {
429
const response = await fetch(`/api/items?page=${page}`);
430
const newItems = await response.json();
431
setItems(prev => [...prev, ...newItems]);
432
setPage(prev => prev + 1);
433
},
434
2000 // Prevent rapid successive loads
435
);
436
437
const handleScroll = () => {
438
if (
439
window.innerHeight + window.scrollY >=
440
document.body.offsetHeight - 1000
441
) {
442
throttledLoadMore();
443
}
444
};
445
446
useEffect(() => {
447
window.addEventListener('scroll', handleScroll);
448
return () => window.removeEventListener('scroll', handleScroll);
449
}, []);
450
451
return (
452
<div>
453
{items.map((item: any) => (
454
<div key={item.id}>{item.title}</div>
455
))}
456
</div>
457
);
458
}
459
```