A classic debounce function, written in modern Javascript.
Tags: javascript
Published: 2023-05-26 20:54:02

---
title: "A classic debounce function, written in modern Javascript."
published: true
tags: Javascript
date: 2022-09-07 00:00:00 UTC
canonical_url: https://blog.melvinvmegen.com/snippets/debounce
---

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flghn4mg1eh5s350j8gk.png)

## Context
There are many events in JS that are triggered in the blink of an eye.

When you scroll the page, resize the window or even move your mouse, the browser captures dozens and dozens of events per second.

In many cases though, it is not necessary to capture all the intermediate steps because you're usually only interested in capturing the final state (when the user has finished scrolling or resizing the window).

**Debouncing** is a strategy that allows us to improve performance by waiting for a given amount of time to elapse before triggering an event. So when the user stops triggering an event, our code runs.

In most cases, this is not necessary. But, if network requests or DOM changes are involved (e.g. rendering a component), this technique can greatly improve the fluidity of your application.

## Usage

```js
const handleScroll = debounce((event) => {
  // Do something every time a user scrolls
  console.log("Scrolling!");
}, 250);
window.addEventListener('scroll', handleScroll);
```

## Explanation
This function is not the easiest to wrap your head around, especially if you are not used to functional programming! Sure, you can use this function without understanding it, but if you're curious and you should let's find out how it works:

```js
const debounce = (callback, wait) => {
  let timeoutId = null;
  // Takes any number of arguments using spread syntax
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      callback.apply(null, args);
    }, wait);
  };
}
```

Our **debounce** function takes two arguments: a function and a duration in milliseconds.
We want **debounce()** to itself return a function. Functions returning functions always hurts my brain, but it this is how I like to think of it:

Think of it as a chef who creates a recipe to cook a dish. When the chef finishes the recipe, they can either serve the dish or pass it on to another chef. In case of the **debounce()**, it's like the first chef has passed on the recipe to a second chef, who can then use it to cook a new dish. 

The second chef can modify the recipe, add new ingredients, or even use it as a base to create a completely new dish. Similarly, the returned function can modify the original function, add new functionality, or use it as a base to create a new function.

*Notice that the first line of this function initializes a variable, **timeoutId**. This line is executed only once. We plan to call our wrapper function several times, but we only call debounce() at the beginning.*

Each time the wrapper function is called, two things happen:

1. We cancel any existing [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)
So, the very first time the user scrolls, this first step has no effect since there is no active timeout. Fortunately, **clearTimeout** is a very forgiving function; even if there is no setTimeout in progress, it does not complain. It's a no-op - it does nothing.

2. We schedule a [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) for a duration determined by the wait argument. When the timeout expires, we call our callback function with [apply](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply), passing down every arguments.
Once programmed, **setTimeout** returns a number, a reference to the timeout in question. We store this in our **timeoutId** property to be able to clean it later on if needed. Since this variable is instantiated on the provided function which is defined outside the scope of our wrapper function, it persists.

Let's say the user has finished scrolling. A few milliseconds (corresponding to the given **wait parameter**) pass, and our wrapper is called again.

This time, **timeoutId** points to a scheduled timeOut, so the first step cancels it before scheduling a new one.

If the user scrolls again, this cycle will repeat itself infinitely. A lot of timeOuts are scheduled and immediately cancelled. Scheduling and cleaning up timeouts is a very fast and low memory cost operation, so we don't have to worry about the cost.

## Conclusion

The final snippet in a compact expression :

```js
const debounce = (callback, wait) => {
  let timeoutId = null;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      callback.apply(null, args);
    }, wait);
  };
}
```