Skip to the code
cf.

Debounce with leading, trailing, and maxWait

HardTiming

Write debounce(fn, wait, { leading, trailing, maxWait }). Calls come in bursts. A burst starts with a call made when no burst is going on, and every call in it restarts a wait ms timer. The burst ends when that timer fires, that is, after wait ms without calls.

leading (default false) calls fn right away, during the call that starts a burst, with that call's this and arguments.

trailing (default true) calls fn when the burst ends, with the this and arguments of the latest call, but only if a call came after fn last ran. So with both on, a burst of one call runs fn once, and a longer burst runs it at both edges.

maxWait (optional, never less than wait) is the longest a burst can last. When a burst has lasted maxWait ms, fn runs with the latest call's this and arguments (if a call came after fn last ran), even though calls keep coming, and the burst is over: the next call starts a new one.

Add a cancel() method that drops any pending call and ends the burst.

const save = debounce(saveDraft, 1000, { maxWait: 5000 });
// saves 1 s after typing stops, and at least every 5 s while typing goes on
 
// wait = 100, calls at 0, 50, and 100 ms:
// {}                                  -> fn runs at 200
// { leading: true, trailing: false }  -> fn runs at 0
// { leading: true }                   -> fn runs at 0 and at 200
// { maxWait: 120 }                    -> fn runs at 120, with the call from 100

What the tests check

  1. by default, calls fn once, wait ms after the last call
    const clock = useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100);
    debounced(1);
    clock.tick(50);
    debounced(2);
    clock.tick(99);
    expect(fn).toHaveBeenCalledTimes(0);
    clock.tick(1);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith(2);
    
  2. leading: true calls fn right away when a burst starts
    useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100, { leading: true });
    debounced(1);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith(1);
    debounced(2);
    expect(fn).toHaveBeenCalledTimes(1);
    
  3. every call extends the burst, so leading runs once per burst
    const clock = useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100, { leading: true, trailing: false });
    debounced(1);
    clock.tick(60);
    debounced(2);
    clock.tick(60);
    debounced(3);
    clock.tick(1000);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1]);
    debounced(4);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1, 4]);
    
  4. with leading and trailing, a single call runs fn once
    const clock = useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100, { leading: true, trailing: true });
    debounced(1);
    clock.tick(1000);
    expect(fn).toHaveBeenCalledTimes(1);
    
  5. with leading and trailing, a burst runs fn at both edges
    const clock = useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100, { leading: true, trailing: true });
    debounced(1);
    clock.tick(50);
    debounced(2);
    clock.tick(99);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1]);
    clock.tick(1);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1, 2]);
    
  6. maxWait runs fn while calls keep coming
    const clock = useFakeTimers();
    const start = clock.now();
    const runs = [];
    const debounced = debounce((value) => runs.push(value + " at " + (clock.now() - start)), 100, { maxWait: 200 });
    for (let i = 0; i < 10; i++) {
      debounced(i); // a call every 50 ms, from 0 to 450
      clock.tick(50);
    }
    clock.tick(1000);
    expect(runs).toEqual(["3 at 200", "7 at 400", "9 at 550"]);
    
  7. keeps the this and arguments of the latest call
    const clock = useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100);
    const a = { debounced };
    const b = { debounced };
    a.debounced(1);
    b.debounced(2, 3);
    clock.tick(100);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn.calls[0].this).toBe(b);
    expect(fn).toHaveBeenLastCalledWith(2, 3);
    
  8. cancel() drops the pending call and ends the burst
    const clock = useFakeTimers();
    const fn = spy();
    const debounced = debounce(fn, 100, { leading: true });
    debounced(1);
    debounced(2);
    debounced.cancel();
    clock.tick(1000);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1]);
    debounced(3);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1, 3]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).