Telerik blogs

Debouncing is a way of skipping input garbage and waiting for things to calm before running a function. Make one debounce function and reuse it often.

Sometimes when you write a program, you need to wait a few seconds before doing anything. This is certainly the case when fetching data, re-fetching data and updating data. You may have 50 triggers at once, when all you need is one.

Debouncing is a way of skipping all this input garbage and waiting for things to calm. Then, and only then, will the expected function run.

Timer

The best example and way to show debouncing is with a timer. Let’s say we want to run the function getData() at the appropriate time.

Step 1: setTimeout

First we need to wait a good time, say 5 seconds, before running the function.

setTimeout(() => console.log('5 seconds'), 5000);

This will make sure nothing gets run until 5 seconds pass. This is great, but we may need to cancel this.

Step 2: clearTimer

const timeout = setTimeout(() => console.log('5 seconds'), 5000);
clearTimeout(timeout);

If we run this, it will allow us to clear the timeout. This means we are canceling running the function inside setTimeout However, since we don’t have any mechanisms to continue, it will always get cleared.

Step 3: Function

Here we put the timeout inside of a function. If the timeout exists, it will get cleared out. Notice we have to declare the timeout variable outside of the run function in order for it to persist. Otherwise it would just declare a new timeout every time and would never get canceled.

let timeout: NodeJS.Timeout;

const run = () => {
	clearTimeout(timeout);
	timeout = setTimeout(() => console.log('5 seconds'), 5000);
};

run();
run();
run();

Since run is ran three times in a row, the timeout does not have time to complete. It will get run before 5 seconds has elapsed. The first two run functions get canceled, and the third one runs as expected. This is what prevents extraneous calls. This is called debouncing.

Step 4: Debounce

If we are using this one time in our program and we never thing we will ever use this function again (highly unlikely for any web programmer), then we could just stop here:

let timeout: NodeJS.Timeout;

const debounce = () => {
	clearTimeout(timeout);
	timeout = setTimeout(() => getData(), 5000);
};

debounce();
debounce();
debounce();

However, we would be breaking Single Responsibility Principal, where we could easily make this a module and reusable. I have found after more than 20 years of programming, this one principal saves me the most time.

Step 5: useDebounce

The problem with creating a reusable module is the timeout variable. We need to make sure it gets reused. We also don’t want to re-pass the timeout and function.

DON’T DO THIS!!!

let timeout: NodeJS.Timeout;

	const getData = () => {
		console.log('getting data...');
	};

	const debounce = (func: () => void, waitfor: number) => {
		clearTimeout(timeout);
		timeout = setTimeout(() => func(), waitfor);
	};

	debounce(getData, 5000);
	debounce(getData, 5000);
	debounce(getData, 5000);

So we need a function we can import, set up our variables, then call whenever and as many times as we like:

useDebounce.ts

export function useDebounce<F extends (...args: Parameters<F>) => ReturnType<F>>(
    func: F,
    waitFor: number,
): (...args: Parameters<F>) => void {
    let timeout: NodeJS.Timeout;
    return (...args: Parameters<F>): void => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), waitFor);
    };
}

We can import this function anywhere we like, and reuse it in any framework!

Step 6: Usage

import { useDebounce } from 'use-debounce';

const getData = () => {
	console.log('getting data...');
};

const debounce = useDebounce(getData, 5000);

debounce();
debounce();
debounce();

As you can see, we get reusable code with the same results. No need to keep track of timers or repass our data.

Example

Autosave

One usage maybe for autosaving drafts:

const saveDraft = () => {
   // save draft
};

const debounce = useDebounce(saveDraft, 5000);

...

<input type="text" oninput="debounce()" />

You may see onchange or onkeyup as well here. This would be the same pattern for:

  • Search suggestions as you type
  • Dropdown autocomplete

This generally would be a good idea for anything you have to calculate as well. If you have a game, you may not want to recalculate the position until a player gets done moving. You will see many examples with mouse movements. There are endless use cases.

Other Notes

  • RXJS has a debounce used for observables. This is great when you want to cancel a subscription as you get more coming in.
  • Lodash has this built in, but I’m not a fan of importing more than you need.

Getting debounce to work is not always as straightforward as it seems. You must declare a function that returns a function. Also, getting types in TypeScript to align with ESLint may not be as evident in other versions. I tried to give a version that can work anywhere.

Debouncing is a necessity for any intermediate or above programmer. Use and reuse this function, and you will never have to waste time again thinking about it.


About the Author

Jonathan Gamble

Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.

 

 

Related Posts

Comments

Comments are disabled in preview mode.