DEV Community

Monaye Win
Monaye Win

Posted on • Updated on

Refactor davidwalsh's debounce function using ES6 arrow and more

We all know Davidwalsh's debounce function. The post is from 2014 but even now many developers use this and most of the Youtube tutorials are based on this.
If you are not familiar with Davidwalsh debounce function take a look at it here:
https://davidwalsh.name/javascript-debounce-function

When I looked at this, it didn't strike me. I have to bang my head many times to understand why he ended up writing the way it is.

So I ended up refactoring the code using a new ES6 arrow function.

Our brains are not made the same way, so some people may get my function better and others don't. Important thing is, you understand what you are writing and your team agrees.

That being said. here we go

const debounce = (func, delay) => {
  let timerId; 
  return () => {
    clearTimeout(timerId);
    timerId = setTimeout(func, delay); // start the timer
  };
};
Enter fullscreen mode Exit fullscreen mode

That's it??!! Yes! this is the bare minimum version of the debounce. It utilize the closures to store the timerId in the parent scope.
You can try it on the sandbox here: https://codesandbox.io/s/es6-debounce-example-llgu7?file=/src/index.js

Davidwalsh's debounce function has more features built in.
Now, instead of adding all features to make the function complex, let's isolate the features, so we can better understand how each feature effects the function. At the end we combine both features into single function.

  1. pass arguments to the function
  2. execute function immediately then delay

Argument Version

const debounce = (func, delay) => {
  let timerId; // keep track of current timer

  // return the function
  return (...args) => {
    const boundFunc = func.bind(this, ...args);
    clearTimeout(timerId);
    timerId = setTimeout(boundFunc, delay); // start the timer
  };
};
Enter fullscreen mode Exit fullscreen mode

This was easy to add. We just needed to bind the arguments to the function.
https://codesandbox.io/s/es6-debounce-arguments-example-2p4bp?file=/src/index.js

Immediate Version

const debounce = (func, delay) => {
  let timerId;
  return () => {
    if (!timerId) {
      func();
    }
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      timerId = null; 
    }, delay);
  };
};
Enter fullscreen mode Exit fullscreen mode

As you can see, on the initial call, we execute the function immediately then set the timerId with the callback function that will null the timerId after the delay.
Here is the immediate version of the sandbox:
https://codesandbox.io/s/es6-debounce-immediate-example-737vm?file=/src/index.js

Combined all

const debounce = (func, delay, immediate) => {
  let timerId;
  return (...args) => {
    const boundFunc = func.bind(this, ...args);
    clearTimeout(timerId);
    if (immediate && !timerId) {
      boundFunc();
    }
    const calleeFunc = immediate ? () => { timerId = null } : boundFunc;
    timerId = setTimeout(calleeFunc, delay);
  }
}
Enter fullscreen mode Exit fullscreen mode

for the bonus, we can change this to throttle as well. Only differences is, timing of resetting the timerId. For throttle, we don't clearTimeout, we just null the timerId after the execution of the function.

const throttle = (func, delay, immediate) => {
  let timerId;
  return (...args) => {
    const boundFunc = func.bind(this, ...args);
    if (timerId) {
      return;
    }
    if (immediate && !timerId) {
      boundFunc();
    }
    timerId = setTimeout(() => {
      if(!immediate) {
        boundFunc(); 
      }
      timerId = null; // reset the timer so next call will be excuted
    }, delay);
  }
}
Enter fullscreen mode Exit fullscreen mode

https://codesandbox.io/s/es6-throttle-example-2702s?file=/src/index.js

Discussion (5)

Collapse
martinhinze profile image
martin-hinze

Hey Monaye,
your simplified version of the function without the immediate argument is very helpful, thank you, I learned a lot and begin understanding debouncing (slowy).
I like how you refer to David Walsh's original post as some classical legacy.
However, it is not from 2004, but from 2014. :-)
Best wishes,
Martin

Collapse
monaye profile image
Monaye Win Author

Oops.. Thank you :wink

Collapse
kelvinzhao profile image
Kelvin Zhao

How would you refactor the 'once' function?

function once( fn, context ) {
    let result
    return function() {
        if( fn ) {
            result = fn.apply( context || this, arguments )
            fn = null
        }
        return result
    }
}
Collapse
thexsdev profile image
thexs-dev

I will be using your improved ES6 debounce version from now on ... Thanks!

On a side note
There is a less popular but quite useful function async-debounce from Julian Gruber that

  • not just run after no calls to it have happened for x milliseconds
  • but also skips calls while the function is currently running to avoid concurrency
  • like when the debounced function calls an Api that take some time to respond (e.g. querying or heavy filtering responding to user's keystrokes)

Is there a ES6 approach to that or another way of achieving that same result?

Collapse
n4ks profile image
n4ks

This is a very useful function and I think that as many developers as possible should know about it. Thanks for the update!