es-toolkit
es-toolkit copied to clipboard
Support for `throttleAsync` function
What if we added a throttleAsync function to throttle asynchronous functions?
Suggesting
I suggest implementing a promise-based throttling function that only allows a new invocation when the previous one has completed.
export function throttleAsync<F extends (...args: any[]) => Promise<any>>(
func: F,
{ signal, throwOnCancel = false }: ThrottleAsyncOptions = {}
): ((...args: Parameters<F>) => Promise<ReturnType<F> | null>) & { cancel: () => void } {
let isRunning = false;
let isCancelled = false;
if (signal) {
signal.addEventListener('abort', () => {
isCancelled = true;
}, { once: true });
}
function cancel(): void {
isCancelled = true;
}
const throttled = async function(...args: Parameters<F>): Promise<ReturnType<F> | null> {
if (isCancelled) {
if (throwOnCancel) {
throw new Error('Operation cancelled');
}
return null;
}
if (isRunning) {
return null;
}
isRunning = true;
try {
const result = await func(...args);
return result;
} finally {
isRunning = false;
}
} as ((...args: Parameters<F>) => Promise<ReturnType<F> | null>) & { cancel: () => void };
throttled.cancel = cancel;
return throttled;
}
Why this is needed
The current throttle function is time-based, which works well for UI events but has limitations with asynchronous operations. Consider this common scenario:
// Current approach with time-based throttle
const throttledFetch = throttle(fetchData, 1000);
throttledFetch('/api/users'); // Executes and takes 2 seconds to complete
// 1.5 seconds later
throttledFetch('/api/users'); // Executes again because 1 second has passed
// But the first request hasn't finished yet!
This can lead to:
- Multiple simultaneous API calls
- Race conditions
- Duplicate form submissions
With the proposed throttleAsync:
// Using promise-based throttleAsync
const throttledFetch = throttleAsync(fetchData);
throttledFetch('/api/users'); // Executes and takes 2 seconds
throttledFetch('/api/users'); // Ignored because previous call is still in progress
// 2 seconds later when first call completes
throttledFetch('/api/users'); // Now allowed to execute
Real-world examples
Preventing duplicate form submissions
const form = document.querySelector('form');
const submitForm = async (event) => {
event.preventDefault();
const data = new FormData(form);
return fetch('/api/submit', { method: 'POST', body: data });
};
// Prevent multiple submissions while one is in progress
const throttledSubmit = throttleAsync(submitForm);
form.addEventListener('submit', throttledSubmit);
Avoiding duplicate API calls
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Create throttled fetch that prevents duplicate calls
const fetchUserData = useCallback(
throttleAsync(async (id) => {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
setUser(data);
}),
[]
);
// Even if this effect runs multiple times in quick succession,
// only one API call will be made at a time
useEffect(() => {
fetchUserData(userId);
}, [userId, fetchUserData]);
}
I think this would be a useful addition to complement the existing throttle function, as it addresses a different but common use case in modern web applications.