Angular + debounceTime

By Arvind Rai, December 31, 2018
debounceTime is a RxJS operator that emits latest value from the source Observable after a given time span has passed without another source emission. It behaves same as RxJS delay but emits only the latest value. debounceTime delays the values emitted by source Observable for the given due time and within this time if a new value arrives, the previous pending value is dropped. In this way debounceTime keeps track of most recent value and emits that most recent value when the given due time is passed. debounceTime is an RxJS operator declared as following.
debounceTime(dueTime: number, scheduler: Scheduler): Observable 
Angular 6 uses RxJS 6. Angular 6 onwards, debounceTime is imported as following.
import { debounceTime } from 'rxjs/operators'; 
It is used with pipe operator of Observable. debounceTime is useful in operation where user changes inputs frequently such as search operation.

Technologies Used

Find the technologies being used in our example.
1. Angular 7.0.0
2. Angular CLI 7.0.3
3. TypeScript 3.1.1
4. Node.js 10.3.0
5. NPM 6.1.0
6. RxJS 6.3.3
7. In-Memory Web API 0.6.1

Understanding debounceTime

Suppose we have a simple form with an input field to take input from user to search a book.
<form [formGroup]="bookForm">
  ID: <input formControlName="bookId">
</form> 
We are fetching user input for every change in the input box.
this.bookId.valueChanges.pipe(
  tap(val => console.log(val))
).subscribe(data => this.userInput = data); 
When user enters a value in the input box, for every character add or delete, we get user input by using valueChanges that returns Observable instance.
If we want to process latest user input with a certain time gap from the time the last user input processing has taken place, and not for every user input change, then we can use RxJS debounceTime as following.
this.bookId.valueChanges.pipe(
  debounceTime(2000),
  tap(val => console.log(val))
).subscribe(data => this.userInput = data); 
Suppose a user is entering data into input box continuously then in the above code, tap will get latest user input only after 2 seconds from time the previous user input has processed. This is because debounceTime will not emit value for the given due time. Once debounceTime has emitted value then next value will be emitted only after 2 seconds with latest value in the case user is continuously entering data into input box.

Using debounceTime with switchMap

Suppose we want to search book for the keyword entered by user. It is possible that user may change keyword frequently and in that case every time search operation will be performed. This operation may be costly because it may involve HTTP hit, database hit etc. Suppose we want search operation on change input value and user wants to search for book id. To avoid frequent hit, we may use debounceTime as following.
this.bookId.valueChanges.pipe(
  debounceTime(1000),
  switchMap(id => {
	console.log(id);
	return this.bookService.getBook(id);
  })
).subscribe(res => this.book = res); 
In the above code, next search operation will be performed with latest input data, once there is a 1 second gap between two consecutive entered values by user.

Using debounceTime with fromEvent

fromEvent creates an Observable instance that emits events of given type coming from the given event target. In our example we have a <div> element. We will catch mouse over event.
fromEvent(document.getElementById('myelement'), 'mouseover').pipe(
   debounceTime(1000),
   map(data => data.srcElement)
).subscribe(val => console.log(val)); 
Find the HTML code.
<div id="myelement">
  Mouse over
</div> 
On mouse over, fromEvent code executes. It is possible that we may perform mouse over operation multiple times in a very short span of time and hence complete process will execute for that number of times. To avoid unnecessary execution, we can use debounceTime. In above code we have used debounceTime with 1 second due time. For two consecutive mouse over operation, only after 1 second the code will execute if the code has executed already.

Complete Example

book.component.ts
import { Component, OnInit } from '@angular/core';
import { fromEvent } from 'rxjs';
import { switchMap, debounceTime, tap, map } from 'rxjs/operators';

import { BookService } from './book.service';
import { Book } from './book';
import { FormControl, FormBuilder, FormGroup } from '@angular/forms';

@Component({
   selector: 'app-book',
   template: `
    <div id="myelement">
      Mouse over
    </div>   
    <h3>Search Book By Id</h3>
    <form [formGroup]="bookForm">
      ID: <input formControlName="bookId">
    </form>
    <div *ngIf="book">
      Id: {{book.id}}, Name: {{book.name}}, Category: {{book.category}}
    </div>
   `
})
export class BookComponent implements OnInit { 
   book: Book;
   constructor(private bookService: BookService, private formBuilder: FormBuilder) { }
   ngOnInit() {
     this.searchBook();
     fromEvent(document.getElementById('myelement'), 'mouseover').pipe(
       debounceTime(1000),
       map(data => data.srcElement)
     ).subscribe(val => console.log(val));
   }
   bookId = new FormControl(); 
   bookForm: FormGroup = this.formBuilder.group({
      bookId: this.bookId
   });   
   searchBook() {
    this.bookId.valueChanges.pipe(
      debounceTime(1000),
      switchMap(id => {
        console.log(id);
        return this.bookService.getBook(id);
      })
    ).subscribe(res => this.book = res);
   }
} 
book.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Book } from './book';
import { catchError } from 'rxjs/operators';

@Injectable({
    providedIn: 'root'
})
export class BookService {
    bookUrl = "/api/books";	
    constructor(private http: HttpClient) { }
    getBook(id: number): Observable<Book> {
      return this.http.get<Book>(this.bookUrl + "/" + id).pipe(
        catchError(err => of(null))
      );
    }    
} 
book.ts
export interface Book {
   id: number;
   name: string;
   category: string;
} 
app.component.ts
import { Component } from '@angular/core';

@Component({
   selector: 'app-root',
   template: `
    <app-book></app-book>
   `
})
export class AppComponent { 
} 
test-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';

export class TestData implements InMemoryDbService {
  createDb() {
    let bookDetails = [
      { id: 101, name: 'Angular by Krishna', category: 'Angular' },
      { id: 102, name: 'Core Java by Vishnu', category: 'Java' },
      { id: 103, name: 'NgRx by Rama', category: 'Angular' }
    ];
    return { books: bookDetails };
  }
} 
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent }  from './app.component';
import { BookComponent }  from './book.component';

//For InMemory testing
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { TestData } from './test-data';

@NgModule({
  imports: [     
      BrowserModule,
      HttpClientModule,
      FormsModule,
      ReactiveFormsModule,
      InMemoryWebApiModule.forRoot(TestData)		
  ],
  declarations: [
      AppComponent,
      BookComponent
  ],
  providers: [
  ],
  bootstrap: [
      AppComponent
  ]
})
export class AppModule { } 

Run Application

To run the application, find the steps.
1. Download source code using download link given below on this page.
2. Use downloaded src in your Angular CLI application. To install Angular CLI, find the link.
3. Install angular-in-memory-web-api@0.6.1
4. Run ng serve using command prompt.
5. Access the URL http://localhost:4200

References

RxJS Observable
Angular: The RxJS library
Pipeable Operators

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI









©2023 concretepage.com | Privacy Policy | Contact Us