Skip to content
VersionSize

debounce

The debounce utility creates a version of a function that delays its execution until a specified amount of time has passed since it was last called. This is ideal for handling rapid-fire events like window resizing, scrolling, or keystrokes.

Implementation

View Source Code
ts
import type { Fn } from '../types';
import { assert } from './assert';

export type Debounced<T extends Fn> = ((this: ThisParameterType<T>, ...args: Parameters<T>) => void) & {
  cancel(): void;
  flush(): ReturnType<T> | undefined;
  pending(): boolean;
};

/**
 * Debounce a function (trailing). Use `flush` to invoke immediately,
 * `cancel` to clear, and `pending` to check if an invocation is scheduled.
 */
export function debounce<T extends Fn>(fn: T, delay = 300): Debounced<T> {
  assert(typeof fn === 'function', 'First argument must be a function', {
    args: { fn },
    type: TypeError,
  });
  assert(typeof delay === 'number' && delay >= 0, 'Delay must be a non-negative number', {
    args: { delay },
    type: TypeError,
  });

  let timer: ReturnType<typeof setTimeout> | undefined;
  let lastArgs: Parameters<T> | undefined;
  let lastThis: ThisParameterType<T> | undefined;
  let lastResult: ReturnType<T> | undefined;

  const clearTimer = () => {
    if (timer !== undefined) {
      clearTimeout(timer);
      timer = undefined;
    }
  };

  const invoke = () => {
    clearTimer();
    if (!lastArgs) return undefined; // nothing to invoke
    const args = lastArgs;
    const ctx = lastThis as ThisParameterType<T>;
    lastArgs = undefined;
    lastThis = undefined;
    // biome-ignore lint/suspicious/noExplicitAny: -
    lastResult = fn.apply(ctx as any, args);
    return lastResult;
  };

  const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    lastArgs = args;
    lastThis = this;
    clearTimer();
    timer = setTimeout(invoke, delay);
  } as Debounced<T>;

  debounced.cancel = () => {
    clearTimer();
    lastArgs = undefined;
    lastThis = undefined;
  };

  debounced.flush = () => invoke() as ReturnType<T> | undefined;

  debounced.pending = () => timer !== undefined;

  return debounced;
}

Features

  • Isomorphic: Works in both Browser and Node.js.
  • Efficient: Prevents unnecessary processing by grouping multiple calls into one.
  • Type-safe: Preserves the argument types of the original function.

API

ts
type DebouncedFunction = {
  (...args: any[]): void;
  cancel: () => void;
  flush: () => void;
};

function debounce<T extends (...args: any[]) => any>(fn: T, wait?: number): DebouncedFunction;

Parameters

  • fn: The function you want to debounce.
  • wait: The number of milliseconds to wait for "silence" before actually calling fn (defaults to 300).

Returns

  • A debounced function with two additional methods:
    • cancel(): Cancels any pending execution.
    • flush(): Immediately executes any pending call and cancels the timer.

Examples

Search Input Handling

ts
import { debounce } from '@vielzeug/toolkit';

const search = debounce((query: string) => {
  console.log('Searching for:', query);
  // Perform API call here
}, 300);

// Only the last call will execute after 300ms of inactivity
search('a');
search('ap');
search('app');
search('apple');

Window Resize

ts
import { debounce } from '@vielzeug/toolkit';

const handleResize = debounce(() => {
  console.log('Window resized!');
  // Recalculate layout
}, 250);

window.addEventListener('resize', handleResize);

Using Cancel and Flush

ts
import { debounce } from '@vielzeug/toolkit';

const saveData = debounce((data) => {
  console.log('Saving:', data);
}, 1000);

saveData({ name: 'Alice' });
saveData({ name: 'Bob' });

// Cancel pending save
saveData.cancel();

// Or immediately execute pending save
saveData({ name: 'Charlie' });
saveData.flush(); // Saves 'Charlie' immediately

Implementation Notes

  • The debounced function does not return the result of the original fn, as execution is asynchronous.
  • Each call to the debounced function clears any existing timer and starts a new one.
  • In a Node.js environment, it uses setTimeout under the hood.

See Also

  • throttle: Execute a function at most once in a specified interval.
  • delay: Pause execution for a specified duration.
  • retry: Automatically retry an asynchronous operation.