📚 6 min read
Debouncing Examples ​
This page demonstrates practical examples of implementing and using debouncing patterns to handle rapid-fire events efficiently.
Basic Debouncing ​
typescript
// Basic debounce implementation
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
options: {
leading?: boolean;
trailing?: boolean;
} = {}
): (...args: Parameters<T>) => void {
let timeoutId: NodeJS.Timeout | undefined;
let lastArgs: Parameters<T> | undefined;
return function (this: any, ...args: Parameters<T>): void {
const later = () => {
timeoutId = undefined;
if (options.trailing !== false && lastArgs) {
func.apply(this, lastArgs);
lastArgs = undefined;
}
};
if (!timeoutId && options.leading !== false) {
func.apply(this, args);
} else {
lastArgs = args;
}
clearTimeout(timeoutId);
timeoutId = setTimeout(later, wait);
};
}
// Usage
const handleSearch = debounce(
(query: string) => {
console.log('Searching for:', query);
},
300,
{ trailing: true }
);
// Rapid-fire events
handleSearch('a');
handleSearch('ap');
handleSearch('app');
handleSearch('appl');
handleSearch('apple');
// Only 'apple' will be logged after 300ms
Advanced Debouncing ​
typescript
class DebouncedFunction<T extends (...args: any[]) => any> {
private timeoutId?: NodeJS.Timeout;
private lastArgs?: Parameters<T>;
private lastResult?: ReturnType<T>;
private pending = false;
constructor(
private readonly func: T,
private readonly wait: number,
private readonly options: {
leading?: boolean;
trailing?: boolean;
maxWait?: number;
} = {}
) {}
execute(...args: Parameters<T>): Promise<ReturnType<T>> {
return new Promise((resolve, reject) => {
const later = async () => {
this.timeoutId = undefined;
if (this.options.trailing !== false && this.lastArgs) {
try {
this.lastResult = await this.func.apply(this, this.lastArgs);
resolve(this.lastResult);
} catch (error) {
reject(error);
} finally {
this.lastArgs = undefined;
this.pending = false;
}
}
};
if (!this.timeoutId && !this.pending && this.options.leading !== false) {
this.pending = true;
Promise.resolve(this.func.apply(this, args))
.then((result) => {
this.lastResult = result;
resolve(result);
})
.catch(reject)
.finally(() => {
this.pending = false;
});
} else {
this.lastArgs = args;
if (this.lastResult) {
resolve(this.lastResult);
}
}
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(later, this.wait);
// Handle maxWait
if (this.options.maxWait && this.lastArgs) {
setTimeout(() => {
if (this.lastArgs) {
later();
}
}, this.options.maxWait);
}
});
}
cancel(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
this.lastArgs = undefined;
this.pending = false;
}
flush(): Promise<ReturnType<T> | undefined> {
return new Promise((resolve, reject) => {
if (this.timeoutId && this.lastArgs) {
clearTimeout(this.timeoutId);
Promise.resolve(this.func.apply(this, this.lastArgs))
.then(resolve)
.catch(reject);
} else {
resolve(this.lastResult);
}
});
}
}
// Usage
const searchAPI = new DebouncedFunction(
async (query: string) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
return response.json();
},
300,
{
leading: false,
trailing: true,
maxWait: 1000,
}
);
// Event handler
async function handleSearchInput(event: InputEvent): Promise<void> {
const query = (event.target as HTMLInputElement).value;
try {
const results = await searchAPI.execute(query);
displayResults(results);
} catch (error) {
console.error('Search failed:', error);
}
}
Real-World Example: Form Validation ​
typescript
class FormValidator {
private validators: Map<string, DebouncedFunction<any>> = new Map();
private formState: Map<string, any> = new Map();
private errors: Map<string, string> = new Map();
constructor(
private readonly options: {
debounceWait?: number;
maxWait?: number;
onValidationStart?: (field: string) => void;
onValidationComplete?: (
field: string,
isValid: boolean,
error?: string
) => void;
} = {}
) {}
registerField(
fieldName: string,
validator: (value: any) => Promise<void>
): void {
this.validators.set(
fieldName,
new DebouncedFunction(
async (value: any) => {
this.options.onValidationStart?.(fieldName);
try {
await validator(value);
this.errors.delete(fieldName);
this.options.onValidationComplete?.(fieldName, true);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Validation failed';
this.errors.set(fieldName, errorMessage);
this.options.onValidationComplete?.(fieldName, false, errorMessage);
throw error;
}
},
this.options.debounceWait ?? 300,
{
leading: false,
trailing: true,
maxWait: this.options.maxWait ?? 1000,
}
)
);
}
async validateField(fieldName: string, value: any): Promise<void> {
this.formState.set(fieldName, value);
const validator = this.validators.get(fieldName);
if (validator) {
await validator.execute(value);
}
}
async validateForm(): Promise<boolean> {
const validations = Array.from(this.formState.entries()).map(
([fieldName, value]) => this.validateField(fieldName, value)
);
try {
await Promise.all(validations);
return this.errors.size === 0;
} catch (error) {
return false;
}
}
getErrors(): Map<string, string> {
return new Map(this.errors);
}
}
// Usage
const validator = new FormValidator({
debounceWait: 300,
maxWait: 1000,
onValidationStart: (field) => {
showLoadingIndicator(field);
},
onValidationComplete: (field, isValid, error) => {
hideLoadingIndicator(field);
updateFieldStatus(field, isValid, error);
},
});
// Register validators
validator.registerField('email', async (email: string) => {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
// Check if email is available
const response = await fetch(
`/api/check-email?email=${encodeURIComponent(email)}`
);
if (!response.ok) {
throw new Error('Email already taken');
}
});
validator.registerField('username', async (username: string) => {
if (username.length < 3) {
throw new Error('Username too short');
}
// Check username availability
const response = await fetch(
`/api/check-username?username=${encodeURIComponent(username)}`
);
if (!response.ok) {
throw new Error('Username already taken');
}
});
// Form event handlers
const form = document.querySelector('form');
form?.addEventListener('input', async (event) => {
const input = event.target as HTMLInputElement;
await validator.validateField(input.name, input.value);
});
form?.addEventListener('submit', async (event) => {
event.preventDefault();
if (await validator.validateForm()) {
submitForm();
}
});
Best Practices ​
Cancellable debounce:
typescriptclass CancellableDebounce<T extends (...args: any[]) => any> { private controller = new AbortController(); constructor( private readonly func: T, private readonly wait: number ) {} execute(...args: Parameters<T>): Promise<ReturnType<T>> { this.controller.abort(); this.controller = new AbortController(); const { signal } = this.controller; return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { if (!signal.aborted) { Promise.resolve(this.func.apply(this, args)) .then(resolve) .catch(reject); } }, this.wait); signal.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new Error('Debounced operation cancelled')); }); }); } cancel(): void { this.controller.abort(); } }
Resource cleanup:
typescriptclass ResourceAwareDebounce<T extends (...args: any[]) => any> { private cleanup?: () => void; constructor( private readonly func: T, private readonly wait: number, private readonly resourceManager: ResourceManager ) {} async execute(...args: Parameters<T>): Promise<ReturnType<T>> { // Clean up previous resources if (this.cleanup) { this.cleanup(); } // Acquire new resources const resources = await this.resourceManager.acquire(); this.cleanup = () => { this.resourceManager.release(resources); }; try { return await new Promise((resolve, reject) => { setTimeout(async () => { try { const result = await this.func.apply(this, args); resolve(result); } catch (error) { reject(error); } finally { this.cleanup?.(); } }, this.wait); }); } catch (error) { this.cleanup(); throw error; } } }
Performance monitoring:
typescriptclass MonitoredDebounce<T extends (...args: any[]) => any> { private metrics = { calls: 0, executedCalls: 0, totalDelay: 0, lastExecutionTime: 0, }; constructor( private readonly func: T, private readonly wait: number ) {} async execute(...args: Parameters<T>): Promise<ReturnType<T>> { this.metrics.calls++; const startTime = Date.now(); return new Promise((resolve, reject) => { setTimeout(async () => { try { const result = await this.func.apply(this, args); this.recordMetrics(startTime); resolve(result); } catch (error) { reject(error); } }, this.wait); }); } private recordMetrics(startTime: number): void { this.metrics.executedCalls++; this.metrics.totalDelay += Date.now() - startTime; this.metrics.lastExecutionTime = Date.now(); } getMetrics() { return { ...this.metrics, averageDelay: this.metrics.totalDelay / this.metrics.executedCalls, executionRate: this.metrics.executedCalls / this.metrics.calls, }; } }
Error boundaries:
typescriptclass ErrorBoundaryDebounce<T extends (...args: any[]) => any> { private errorHandler?: (error: Error) => void; private retryCount = 0; private readonly maxRetries = 3; constructor( private readonly func: T, private readonly wait: number ) {} setErrorHandler(handler: (error: Error) => void): void { this.errorHandler = handler; } async execute(...args: Parameters<T>): Promise<ReturnType<T>> { return new Promise((resolve, reject) => { setTimeout(async () => { try { const result = await this.func.apply(this, args); this.retryCount = 0; resolve(result); } catch (error) { this.handleError(error as Error, args) .then(resolve) .catch(reject); } }, this.wait); }); } private async handleError( error: Error, args: Parameters<T> ): Promise<ReturnType<T>> { this.errorHandler?.(error); if (this.retryCount < this.maxRetries) { this.retryCount++; return this.execute(...args); } throw error; } }