React hooks library for debouncing and throttling functionality with small footprint and comprehensive control features
—
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.
Creates a throttled version of a callback function that executes at most once per wait interval.
/**
* Creates a throttled function that only invokes func at most once per
* every wait milliseconds (or once per browser frame).
*
* @param func - The function to throttle
* @param wait - The number of milliseconds to throttle invocations to
* @param options - Optional configuration for leading/trailing execution
* @returns Throttled function with control methods
*/
function useThrottledCallback<T extends (...args: any) => ReturnType<T>>(
func: T,
wait: number,
options?: CallOptions
): DebouncedState<T>;Parameters:
func: T - The function to throttlewait: number - The throttle interval in millisecondsoptions?: CallOptions - Configuration object with:
leading?: boolean - If true, invokes the function on the leading edge (default: true)trailing?: boolean - If true, invokes the function on the trailing edge (default: true)Returns:
DebouncedState<T> - Throttled function with control methods (cancel, flush, isPending)Usage Examples:
import React, { useState, useEffect } from 'react';
import { useThrottledCallback } from 'use-debounce';
// Scroll position tracking
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const throttledScrollHandler = useThrottledCallback(
() => {
setScrollY(window.pageYOffset);
},
100 // Update scroll position at most once every 100ms
);
useEffect(() => {
window.addEventListener('scroll', throttledScrollHandler);
return () => {
window.removeEventListener('scroll', throttledScrollHandler);
};
}, [throttledScrollHandler]);
return (
<div style={{ position: 'fixed', top: 0, left: 0, background: 'white' }}>
Scroll Y: {scrollY}px
</div>
);
}
// Window resize handling
function ResponsiveComponent() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
const throttledResizeHandler = useThrottledCallback(
() => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
},
200 // Recalculate layout at most once every 200ms
);
useEffect(() => {
window.addEventListener('resize', throttledResizeHandler);
return () => {
window.removeEventListener('resize', throttledResizeHandler);
};
}, [throttledResizeHandler]);
return (
<div>
Window size: {windowSize.width} x {windowSize.height}
</div>
);
}
// Mouse movement tracking
function MouseTracker() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const throttledMouseMove = useThrottledCallback(
(event: MouseEvent) => {
setMousePos({ x: event.clientX, y: event.clientY });
},
50 // Update mouse position at most once every 50ms
);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => throttledMouseMove(e);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, [throttledMouseMove]);
return (
<div>
Mouse: ({mousePos.x}, {mousePos.y})
</div>
);
}
// API requests with rate limiting
function RateLimitedRequests() {
const [data, setData] = useState(null);
const [requestCount, setRequestCount] = useState(0);
const throttledFetch = useThrottledCallback(
async (endpoint: string) => {
setRequestCount(prev => prev + 1);
const response = await fetch(endpoint);
const result = await response.json();
setData(result);
},
5000 // Maximum one request every 5 seconds
);
return (
<div>
<button onClick={() => throttledFetch('/api/data')}>
Fetch Data (Throttled)
</button>
<p>Requests made: {requestCount}</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
// Leading edge only (immediate execution, then throttled)
function ImmediateAction() {
const [count, setCount] = useState(0);
const throttledIncrement = useThrottledCallback(
() => {
setCount(prev => prev + 1);
},
1000,
{ leading: true, trailing: false }
);
return (
<div>
<button onClick={throttledIncrement}>
Increment (Immediate, then throttled)
</button>
<p>Count: {count}</p>
</div>
);
}
// Trailing edge only (throttled execution only)
function TrailingAction() {
const [lastAction, setLastAction] = useState('');
const throttledLog = useThrottledCallback(
(action: string) => {
setLastAction(`${action} at ${new Date().toLocaleTimeString()}`);
},
2000,
{ leading: false, trailing: true }
);
return (
<div>
<button onClick={() => throttledLog('Button clicked')}>
Click Me (Trailing only)
</button>
<p>Last action: {lastAction}</p>
</div>
);
}
// Animation frame throttling
function AnimationExample() {
const [rotation, setRotation] = useState(0);
const throttledRotate = useThrottledCallback(
() => {
setRotation(prev => (prev + 10) % 360);
},
16 // ~60fps throttling
);
useEffect(() => {
const interval = setInterval(throttledRotate, 10);
return () => clearInterval(interval);
}, [throttledRotate]);
return (
<div
style={{
width: 100,
height: 100,
background: 'blue',
transform: `rotate(${rotation}deg)`,
transition: 'transform 0.1s ease',
}}
/>
);
}
// Search with throttled API calls
function ThrottledSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const throttledSearch = useThrottledCallback(
async (searchTerm: string) => {
if (!searchTerm.trim()) {
setResults([]);
return;
}
setIsSearching(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 {
setIsSearching(false);
}
},
800 // Maximum one search every 800ms
);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
throttledSearch(value);
};
return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Search (throttled)..."
/>
{isSearching && <p>Searching...</p>}
<ul>
{results.map((result: any) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}function ThrottleWithControls() {
const [value, setValue] = useState(0);
const throttledUpdate = useThrottledCallback(
(newValue: number) => {
setValue(newValue);
},
1000
);
return (
<div>
<button onClick={() => throttledUpdate(value + 1)}>
Increment (Throttled)
</button>
<button onClick={() => throttledUpdate.cancel()}>
Cancel Pending
</button>
<button onClick={() => throttledUpdate.flush()}>
Execute Now
</button>
<p>Value: {value}</p>
<p>Has Pending: {throttledUpdate.isPending() ? 'Yes' : 'No'}</p>
</div>
);
}interface CallOptions {
/**
* Specify invoking on the leading edge of the timeout
* Default: true for useThrottledCallback
*/
leading?: boolean;
/**
* Specify invoking on the trailing edge of the timeout
* Default: true for useThrottledCallback
*/
trailing?: boolean;
}Throttling ensures a function executes at regular intervals during continuous activity:
Debouncing delays execution until activity stops:
// Throttling - executes regularly during scrolling
const throttledScroll = useThrottledCallback(
() => console.log('Scroll position:', window.scrollY),
100 // Every 100ms while scrolling
);
// Debouncing - executes once after scrolling stops
const debouncedScroll = useDebouncedCallback(
() => console.log('Scroll ended at:', window.scrollY),
100 // 100ms after scrolling stops
);function PerformanceMonitor() {
const [fps, setFPS] = useState(0);
const frameCount = useRef(0);
const lastTime = useRef(performance.now());
const throttledFPSUpdate = useThrottledCallback(
() => {
const now = performance.now();
const delta = now - lastTime.current;
const currentFPS = Math.round((frameCount.current * 1000) / delta);
setFPS(currentFPS);
frameCount.current = 0;
lastTime.current = now;
},
1000 // Update FPS display once per second
);
useEffect(() => {
const animate = () => {
frameCount.current++;
throttledFPSUpdate();
requestAnimationFrame(animate);
};
const id = requestAnimationFrame(animate);
return () => cancelAnimationFrame(id);
}, [throttledFPSUpdate]);
return <div>FPS: {fps}</div>;
}function ThrottledEventDelegation() {
const throttledHandler = useThrottledCallback(
(event: Event) => {
const target = event.target as HTMLElement;
if (target.matches('.interactive')) {
console.log('Interacted with:', target.textContent);
}
},
200
);
useEffect(() => {
document.addEventListener('click', throttledHandler);
return () => {
document.removeEventListener('click', throttledHandler);
};
}, [throttledHandler]);
return (
<div>
<div className="interactive">Button 1</div>
<div className="interactive">Button 2</div>
<div className="interactive">Button 3</div>
</div>
);
}function ProgressiveLoader() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const throttledLoadMore = useThrottledCallback(
async () => {
const response = await fetch(`/api/items?page=${page}`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
},
2000 // Prevent rapid successive loads
);
const handleScroll = () => {
if (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 1000
) {
throttledLoadMore();
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div>
{items.map((item: any) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-use-debounce