SI

March 22, 2019

Writing Your Own Debouncer Function

JavaScript

4 min read

A couple of years ago, I was asked to write my own debouncer function at an interview. Unfortunately, I didn't have enough time to complete this question, so, I wanted to spend sometime to solve this problem on my own.

So, what is a debounced function?

A debounced function is a delayed function that can only be invoked if a specified amount of time has passed between successful calls. This is often useful when you want to prevent the number of API calls that are made when implementing something like an autocomplete field.

What is a debouncer function?

A debouncer function is a function that takes two arguments -

  1. a callback (i.e. the function you want to debounce)
  2. an amount of time in milliseconds between successful calls

and returns a new "debounced" version of the function passed in as the first argument.

The Code

As aforementioned, we want to return a new function from the debouncer:

function debounce(callback, timeout) {
  return (...args) => {
    // ...
  }
}

Since the new function can contain any number of arguments, we will collect all arguments in an array by using the ES6 spread syntax which we will call args.

Now, we want to "delay" the function call of callback by timeout milliseconds. This part is easy, since we can use setTimeout to simulate a delayed function call.

function debounce(callback, timeout) {
  return (...args) => {
    setTimeout(() => {
      callback.apply(null, args)
    }, timeout)
  }
}

We are getting close but, you will immediately notice a bug if you try to use our debouncer function in its current state.

<button id="debounced-button">click me</button>
const debouncedHandler = debounce(() => console.log('Clicked!'), 1000)

const button = document.getElementById('debounced-button')

button.addEventListener('click', debouncedHandler)
Bug in our debouncer function.

Everytime we click the button, we create a new setTimeout that will invoke the callback as soon as timeout milliseconds has passed. Therefore over time, our callback will be invoked as many times as we clicked it. This is not the intended behavior. We need to figure out a way to "cancel" any previous setTimeouts, so, we only successfully invoke callback after the delay has entirely elapsed without interruption. Canceling a setTimeout is easy using the clearTimeout function that will clear a setTimeout so long as you pass the unique ID returned from the setTimeout. But how do we save the previous setTimeout? I think this is the real challenge of this interview question. It is actually quite simple! We just need to make use of function closures.

function debounce(callback, timeout) {
  let timeoutRef
  let value
  return (...args) => {
    clearTimeout(timeoutRef)
    timeoutRef = setTimeout(() => {
      value = callback.apply(null, args)
    }, timeout)
    return value
  }
}

Let's break this code down further.

  1. We declare two new variables timeoutRef and value outside of the debounced function to hold the ID of the setTimeout, and the callback return value, respectively.
  2. We clear any stale setTimeout by calling clearTimeout with the ID associated with the previous setTimeout. You might be wondering - "But what if timeoutRef is undefined?" For example, on the first run? clearTimeout will not do anything in that case and will move on to the next line of code.
  3. We save the ID returned from the new setTimeout into the timeoutRef variable.
  4. finally we save the return value of the callback function invoked with args, so, that we can return this value from the debounced function (I am using the apply method on the Function prototype, so, that we can invoke callback with the args array. You can read more about apply here.)
Working debouncer function.

And there you have it, a custom debouncer function! This is definitely a great interview question to test the candidates understanding of function closures. I hope this helps you as well in one of your future interviews!


a not so professional head shot.

Satoshi

👋 Hello, thanks for the read! If you found my work helpful, have constructive feedback, or just want to say hello, connect with me on social media. Thanks in advance!

© 2021 Made by Scott Iwako