Angular + RxJS - debounce
February 04, 2024
On this page we will learn to use RxJS debounce
operator in our Angular application. debounce
operator is used to delay the emission from source observable and emits only the latest notification. The waiting time of debounce
is decided by another observable. When we use debounce
, the notification from source observable emits only after a time span determined by another observable has passed without another source emission.
Here I will discuss
debounce
in detail with examples.
1. debounce
Find thedebounce
operator construct from RxJS API.
debounce<T>(durationSelector: (value: T) => ObservableInput<any>): MonoTypeOperatorFunction<T>
durationSelector is a
Promise
or an Observable
to decide the waiting time. durationSelector is a function that receives a value from source observable to compute timeout duration for each source value.
Returns :
debounce
operator returns an Observable
which emits after a delay specified by observable returned by durationSelector function.
1. When source observable emits notification,
debounce
operator keeps it in waiting for due time and if another notification is emitted by source observable before waiting time of previous notification is finished, then previous notification is discarded and latest notification start waiting for the due time decided by durationSelector
.
2.
debounce
operator keeps only latest notification from the source observable and spawns duration observable by calling durationSelector
. The debounce
operator will emit notification only when the duration observable emits a next notification and no notification was emitted since the duration observable was spawned.
3. If source observable emits a new notification before the duration observable emits a notification, then the previous notification is discarded and a new duration will be scheduled by duration observable.
4. If the completing event happens during the scheduled duration, the last cached notification emits before the completion event is forwarded to output observable and if error event happens during the scheduled duration, only the error event is forwarded.
2. Using debounce
Fordebounce
operator demo, I am creating a FormControl
instance and binding it to HTML input text element. Using valueChanges
, I will get values entered by user. valueChanges
will emit values for every change in input text.
Here our source observable is
valueChanges
that will emit notification on every value change in input box.
I am creating duration observable using RxJS
interval
operator that creates an observable to emit sequential numbers every specified interval of time.
HTML :
<input [formControl]="msg">
msg = new FormControl(); ngOnInit() { this.msg.valueChanges.pipe( debounce(v => interval(Math.random() * 3000)) ).subscribe(v => { console.log(v); }) }
interval
is creating new duration observable.
Suppose
Math.random()
returns 0.5 then interval
will get specified time as 0.5 * 3000 = 1500 ms. The interval(1500)
creates duration observable of 1500 ms. Within this period all the values entered by user in input text, will be discard and only the last one will be kept that will be emitted once 1500 ms is passed after last value change by user.
3. debounce vs debounceTime
1.debounce
and debounceTime
both operators are used to delay source emit but they differ in the way delay time is provided.
2. Delay time in
debounceTime
is a specified number whereas the delay time in debounce
is obtained by another observable.
3.
debounce
and debounceTime
both are rate-limiting and delay-like operators because they do not emit necessarily at the same time when source observable emits.
4. Find the code with
debounceTime
.
this.msg.valueChanges.pipe( debounceTime(1000) ).subscribe(v => { console.log(v); })
debounce
.
this.msg.valueChanges.pipe( debounce(v => interval(1000)) ).subscribe(v => { console.log(v); })
4. debounce + switchMap Example
RxJSswitchMap
emits only latest inner observable in the case when before completing an inner observable new inner observable starts executing. Using debounce
we can avoid creating inner observable by switchMap
for every user input change. If inner observable is hitting HTTP request, that cost will be reduced by using debounce
operator.
Here in our example, we have an application to fetch employee detail. User will enter employee id and employee detail will be displayed. Using
debounce
we can avoid HTTP hit for every input, for example suppose user want to enter employee id 111 then user will enter 1, 11, 111 and then he will stop. So for 1 and 11 values there should not be HTTP hit that we can achieve by using debounce
operator.
Find the code.
emp.component.ts
import { Component, OnInit } from '@angular/core'; import { EmpService } from './services/emp.service'; import { CommonModule } from '@angular/common'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Observable, debounce, interval, switchMap } from 'rxjs'; @Component({ selector: 'my-app', standalone: true, imports: [CommonModule, ReactiveFormsModule], templateUrl: './emp.component.html' }) export class EmpComponent implements OnInit { empId = new FormControl(); constructor(private empService: EmpService) { } result$!: Observable<any[]>; ngOnInit() { this.result$ = this.empId.valueChanges.pipe( debounce(() => interval(1000)), switchMap(id => this.empService.findEmpById(id)) ); } }
<input [formControl]="empId"> <br/><br/> <div *ngIf="result$ | async as emp"> {{emp | json}} </div>
import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class EmpService { constructor(private http: HttpClient) { } findEmpById(id: number): Observable<any[]> { return this.http.get<any[]>("/api/getEmpById", { params: new HttpParams().set('id', id) }); } }
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class TestData implements InMemoryDbService { createDb() { let empDetails = [ { id: '111', name: 'Ritesh', city: "Varanasi" }, { id: '222', name: 'Suresh', city: "PrayagRaj" } ]; return { getEmpById: empDetails }; } }