Building the throttle function

Most of the time, we find the throttle function being compared to debounce, which we created as the first article in this series.

That's because both function accomplish a similar task, to save resources while handling function calls based on a specific timer the user is able to set. But there is a key difference at play:

  • debounce will allow you to postpone the function call until there are no more event that calls them within a specified interval. Quick example: the user is typing on an input field and you do not want to run your function at every keystroke but each second where user stops writing.

  • throttle instead ensures that a function executes at most once every interval. Quick example: you have a button that fires a complex operation, and you want to start the process as soon as the user clicks it, but you do not want to consider following clicks for a specific interval.

While I think you should also give the user UI feedback on what's happening, like showing a loading spinner while the debounced function starts or disabling the button while the throttled function is being called, let's focus on how we could implement such function.

As usual, this article is part of my series, where I discuss how I've solved the challenges inside GFE 75. You can find all the information inside the GreatFrontEnd platform, go there and have a look because more than 80% of the content and features are free ๐Ÿ˜‰

How I solved the challenge

As I wrote in the introduction, debounce and throttle are similar because they allow us to save resources. Still, since they basically work in the opposite direction in this case, I couldn't leverage the code I had previously written. Well, not the main structure, at least.

One thing debounce and throttle are similar about is that they return a function, and what taught me the latest experience with debounce is that we should be aware of this, which implies no arrow functions.

export default function throttle<T extends any[]>(
  func: ThrottleFunction<T>,
  wait: number,
): ThrottleFunction<T> {
  let waiting = false;

  return function (this: any, ...args: T) {}
}

If you go back to the debounce article, you should see that as its base, both code snippets look similar; each takes in a func and a wait argument and returns a function that has access to this.

But here's where the similarity ends because, as described above, each function has a different purpose, and we can understand it from the name of the first variable we see in each code block.

In debounce, the first variable we met was an undefined numeric timeoutId, while the throttle has a boolean waiting already set to false. It should be simple to understand that while debounce interest is to remove previous timers we had in place waiting to call a function, with throttle, we are focused on do not call the function again if a specific time hasn't passed yet (that means that waiting is true).

I've tried to make as clear as possible the similarities and differences of these functions, now it's time show you how I've implemented throttle.

export default function throttle<T extends any[]>(
  func: ThrottleFunction<T>,
  wait: number,
): ThrottleFunction<T> {
  let waiting = false;

  return function (this: any, ...args: T) {
    if(!waiting){
      func.apply(this, args);
      waiting = true;
    }

    // Other code
  }
}

When the user calls the throttled function, we need to understand whether we're inside a time window where the function cannot be called. For example, as soon as the user run the returned funciton, we need to execute func.

So in the above snippet, we first check for the waiting value. If we're not waiting (like first time function gets called or wait time expires), we run func attaching this to it and we set the waiting value to true since now we need to wait time expiration to call func again.

But how do we know if enough time has passed?

If we keep the function definition like so, the end user cannot run the throttled function because there's no way that waiting will get back to false. And that's why we need to add a setTimeout with some logic.

// Other code

setTimeout(() => {
  if(waiting){
    waiting = false;
  } else {
    func.apply(this, args);
    waiting = true;
  }
}, wait)

// Other code

Here's the entire logic. waiting will be true until the setTimeout function will not be fun after wait time expires. Once we can run the function inside it, the first thing we do is check if waiting is still true. If so, we just set waiting to false and call it a day.

Instead, if we still had a pending setTimeout and waiting is false, once wait expires, we will execute func the same way we did at the beginning.

My solution is nothing elegant or clever; it solves all the tests and looks straightforward. Let's check now how it compares with the proposed solutions.

Compare with proposed solutions

I'm always curious to discover how people solve the challenges I just solved, there's so much to learn!

In this case, the thing that I can learn is how to reflect on the question: "How can I make this code simpler and easier to understand?"

I kept thinking about different syntax or functions I could leverage; even though apply does a terrific job here, the part I was missing if I compare mine with the proposed solution is the way I was tackling the problem.

My code focused on the waiting state, while the code in the proposed solution is focused on shouldThrottle, and this, as you'll soon discover, incredibly simplifies our code!

export default function throttle(func, wait = 0) {
  let shouldThrottle = false;

  return function (...args) {
    if (shouldThrottle) return;

    shouldThrottle = true;

    setTimeout(function () {
      shouldThrottle = false;
    }, wait);

    func.apply(this, args);
  };
}

All we care about the solution is shouldThrottle state. If that's true we just return from the function stopping following executions, otherwise, we set it to true, we prepare the setTimeout to reset it to false once wait expires, and we call func.apply(this, args).

If you paid close attention, you've also noticed that the returned function of throttle does not need this as an argument, and that's because since func get's called immediately within the context of the wrapping function, we're able to bind this correctly because this context is kept.

I kept the call to action to GreatFrontEnd at a minimum this time, and I want to make it clear that I am not an affiliate of it. I will earn nothing if you subscribe. But it's not for the monetary value that I want to introduce you to the platform; it's because it is a great way to gather and show off your expertise.

0
Subscribe to my newsletter

Read articles from Andrea Barghigiani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andrea Barghigiani
Andrea Barghigiani

Born as WordPress theme developer and switched on React.js while the CMS was still fighting with its own community about the Gutenberg editor. Now working daily building full-stack applications with Next.js