javascript - Angular 2+ and debounce

ID : 20199

viewed : 13

Tags : javascriptangularjavascript

Top 5 Answer for javascript - Angular 2+ and debounce

vote vote

94

Updated for RC.5

With Angular 2 we can debounce using RxJS operator debounceTime() on a form control's valueChanges observable:

import {Component}   from '@angular/core'; import {FormControl} from '@angular/forms'; import {Observable}  from 'rxjs/Observable'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/throttleTime'; import 'rxjs/add/observable/fromEvent';  @Component({   selector: 'my-app',   template: `<input type=text [value]="firstName" [formControl]="firstNameControl">     <br>{{firstName}}` }) export class AppComponent {   firstName        = 'Name';   firstNameControl = new FormControl();   formCtrlSub: Subscription;   resizeSub:   Subscription;   ngOnInit() {     // debounce keystroke events     this.formCtrlSub = this.firstNameControl.valueChanges       .debounceTime(1000)       .subscribe(newValue => this.firstName = newValue);     // throttle resize events     this.resizeSub = Observable.fromEvent(window, 'resize')       .throttleTime(200)       .subscribe(e => {         console.log('resize event', e);         this.firstName += '*';  // change something to show it worked       });   }   ngDoCheck() { console.log('change detection'); }   ngOnDestroy() {     this.formCtrlSub.unsubscribe();     this.resizeSub  .unsubscribe();   } }  

Plunker

The code above also includes an example of how to throttle window resize events, as asked by @albanx in a comment below.


Although the above code is probably the Angular-way of doing it, it is not efficient. Every keystroke and every resize event, even though they are debounced and throttled, results in change detection running. In other words, debouncing and throttling do not affect how often change detection runs. (I found a GitHub comment by Tobias Bosch that confirms this.) You can see this when you run the plunker and you see how many times ngDoCheck() is being called when you type into the input box or resize the window. (Use the blue "x" button to run the plunker in a separate window to see the resize events.)

A more efficient technique is to create RxJS Observables yourself from the events, outside of Angular's "zone". This way, change detection is not called each time an event fires. Then, in your subscribe callback methods, manually trigger change detection – i.e., you control when change detection is called:

import {Component, NgZone, ChangeDetectorRef, ApplicationRef,          ViewChild, ElementRef} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/throttleTime'; import 'rxjs/add/observable/fromEvent';  @Component({   selector: 'my-app',   template: `<input #input type=text [value]="firstName">     <br>{{firstName}}` }) export class AppComponent {   firstName = 'Name';   keyupSub:  Subscription;   resizeSub: Subscription;   @ViewChild('input') inputElRef: ElementRef;   constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef,     private appref: ApplicationRef) {}   ngAfterViewInit() {     this.ngzone.runOutsideAngular( () => {       this.keyupSub = Observable.fromEvent(this.inputElRef.nativeElement, 'keyup')         .debounceTime(1000)         .subscribe(keyboardEvent => {           this.firstName = keyboardEvent.target.value;           this.cdref.detectChanges();         });       this.resizeSub = Observable.fromEvent(window, 'resize')         .throttleTime(200)         .subscribe(e => {           console.log('resize event', e);           this.firstName += '*';  // change something to show it worked           this.cdref.detectChanges();         });     });   }   ngDoCheck() { console.log('cd'); }   ngOnDestroy() {     this.keyupSub .unsubscribe();     this.resizeSub.unsubscribe();   } }  

Plunker

I use ngAfterViewInit() instead of ngOnInit() to ensure that inputElRef is defined.

detectChanges() will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick() instead. (I put a call to ApplicationRef.tick() in comments in the plunker.) Note that calling tick() will cause ngDoCheck() to be called.

vote vote

80

If you don't want to deal with @angular/forms, you can just use an RxJS Subject with change bindings.

view.component.html

<input [ngModel]='model' (ngModelChange)='changed($event)' /> 

view.component.ts

import { Subject } from 'rxjs/Subject'; import { Component }   from '@angular/core'; import 'rxjs/add/operator/debounceTime';  export class ViewComponent {     model: string;     modelChanged: Subject<string> = new Subject<string>();      constructor() {         this.modelChanged             .debounceTime(300) // wait 300ms after the last event before emitting last event             .distinctUntilChanged() // only emit if value is different from previous value             .subscribe(model => this.model = model);     }      changed(text: string) {         this.modelChanged.next(text);     } } 

This does trigger change detection. For a way that doesn't trigger change detection, check out Mark's answer.


Update

.pipe(debounceTime(300), distinctUntilChanged()) is needed for rxjs 6.

Example:

   constructor() {         this.modelChanged.pipe(             debounceTime(300),              distinctUntilChanged())             .subscribe(model => this.model = model);     } 
vote vote

79

Since the topic is old, most of the answers don't work on Angular 6/7/8/9/10 and/or use other libs.
So here is a short and simple solution for Angular 6+ with RxJS.

Import the necessary stuff first:

import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; 

Implement the ngOnInit and ngOnDestroy:

export class MyComponent implements OnInit, OnDestroy {   public notesText: string;   public notesModelChanged: Subject<string> = new Subject<string>();   private notesModelChangeSubscription: Subscription    constructor() { }    ngOnInit() {     this.notesModelChangeSubscription = this.notesModelChanged       .pipe(         debounceTime(2000),         distinctUntilChanged()       )       .subscribe(newText => {         this.notesText = newText;         console.log(newText);       });   }    ngOnDestroy() {     this.notesModelChangeSubscription.unsubscribe();   } } 

Use this way:

<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' /> 

P.S. For more complex and efficient solutions you might still want to check other answers.

vote vote

65

It could be implemented as Directive

import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { NgControl } from '@angular/forms'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import { Subscription } from 'rxjs';  @Directive({   selector: '[ngModel][onDebounce]', }) export class DebounceDirective implements OnInit, OnDestroy {   @Output()   public onDebounce = new EventEmitter<any>();    @Input('debounce')   public debounceTime: number = 300;    private isFirstChange: boolean = true;   private subscription: Subscription;    constructor(public model: NgControl) {   }    ngOnInit() {     this.subscription =       this.model.valueChanges         .debounceTime(this.debounceTime)         .distinctUntilChanged()         .subscribe(modelValue => {           if (this.isFirstChange) {             this.isFirstChange = false;           } else {             this.onDebounce.emit(modelValue);           }         });   }    ngOnDestroy() {     this.subscription.unsubscribe();   }  } 

use it like

<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)"> 

component sample

import { Component } from "@angular/core";  @Component({   selector: 'app-sample',   template: ` <input[(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)"> <input[(ngModel)]="value" (onDebounce)="asyncDoSomethingWhenModelIsChanged($event)"> ` }) export class SampleComponent {   value: string;    doSomethingWhenModelIsChanged(value: string): void {     console.log({ value });   }    async asyncDoSomethingWhenModelIsChanged(value: string): Promise<void> {     return new Promise<void>(resolve => {       setTimeout(() => {         console.log('async', { value });         resolve();       }, 1000);     });   } }  
vote vote

51

Not directly accessible like in angular1 but you can easily play with NgFormControl and RxJS observables:

<input type="text" [ngFormControl]="term"/>  this.items = this.term.valueChanges   .debounceTime(400)   .distinctUntilChanged()   .switchMap(term => this.wikipediaService.search(term)); 

This blog post explains it clearly: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Here it is for an autocomplete but it works all scenarios.

Top 3 video Explaining javascript - Angular 2+ and debounce

Related QUESTION?