DEV Community

Volodymyr Yepishev
Volodymyr Yepishev

Posted on • Originally published at linkedin.com

Debouncing Component Methods in Angular

Using component methods is generally unwise for known reasons, yet sometimes an application can have third party components that require using component methods in template to react to outputs.

In today's article we are going to explore a way of debouncing component's method execution, so it does not fire too often, without resolving to rxjs tools.

For the sake of simplicity we will be using a simple component that implements a counter.

import { Component } from '@angular/core';

@Component({
  template: `<p>{{ calledTimes }}</p>
    <p><button (click)="increment()">increment</button></p>`,
  selector: 'app-root',
})
export class AppComponent {
  private _calledTimes = 0;

  public get calledTimes(): number {
    return this._calledTimes;
  }

  public increment(): void {
    this._calledTimes++;
  }
}
Enter fullscreen mode Exit fullscreen mode

Everything is rather straightforward: there is a property that is incremented each time a handler in template is called. To show things down we could use rxjs' Subjects with debounceTime, but such solution would be limited to a component that implements it and not transportable to other components which might need debouncing their method calls.

What we can do as an alternative, is to create a parameterized decorator which would accept a debounce time in milliseconds and utilize Promise to postpone method execution.

export function debounce(ms: number) {
  return (
    target: unknown,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ): void => {
    let timer: ReturnType<typeof setTimeout>;
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      new Promise((resolve) => {
        if (timer) {
          clearTimeout(timer);
        }

        timer = setTimeout(() => {
          resolve(originalMethod.apply(this, ...args));
        }, ms);
      });
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

What is cool about this approach is that now we have a separate debouncing mechanism which we can apply to any method of any component. So if we wanted to limit increment to 2 seconds, we could simply decorate the method:

import { Component } from '@angular/core';

import { debounce } from './debounce/debounce.decorator';

@Component({
  template: `<p>{{ calledTimes }}</p>
    <p><button (click)="increment()">increment</button></p>`,
  selector: 'app-root',
})
export class AppComponent {
  private _calledTimes = 0;

  public get calledTimes(): number {
    return this._calledTimes;
  }

  @debounce(2000)
  public increment(): void {
    this._calledTimes++;
  }
}
Enter fullscreen mode Exit fullscreen mode

Simple as that :)

Top comments (4)

Collapse
 
greenflag31 profile image
Green

Warning : you forgot a return in front of descriptor.value ...

Collapse
 
bwca profile image
Volodymyr Yepishev

It was deliberate actually: had there been a return, that would change the type of the decorated method to async, which is something IDE would not reflect in intellisense, so this debouncing approach is only viable when decorating functions with void returns :)

Unless I am mistaken and IDEs follow changed return type of decorated methods.

Collapse
 
greenflag31 profile image
Green • Edited

Sorry, I just tested again. Try to call debounce on a function which as an argument. It will be lost and a bug will occur : cannot read property of undefined.
Following is working :

export function debounce(delay: number) {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    let timer: ReturnType<typeof setTimeout>;
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      clearTimeout(timer);

      timer = setTimeout(() => {
        originalMethod.apply(this, args);
      }, delay);
    };
Enter fullscreen mode Exit fullscreen mode
return descriptor;
Enter fullscreen mode Exit fullscreen mode

};
}

Thread Thread
 
bwca profile image
Volodymyr Yepishev

Good point 👍