Application data filtering
Angular Patterns RxJS SPA Typescript Visual Code

Efficient Filtering of Results using RxJS Debounce in Angular Applications

Welcome to today’s post.

In most of my discussions with RxJS so far, I have focused mainly on the efficiency of retrieving data and presenting that to the user interface.  What if we have data that is controlled by the end user? What if the stream of data is in the form of keyboard inputs?

I will show how to use the RxJS filter operator Debounce to solve an important problem we have when typing in input data for incremental searches. That problem is that when we release input to a search service API that cannot process the request and response sufficiently fast enough, the user interface cannot update and refresh the results to the output leading to an unresponsive interface.

I will also show how to use the RxJS filter operator Debounce to filter out search queries that are used to filter out result sets in Angular UI forms.

As you may already be aware of with the RxJS library, we can utilize the pipe() operator to execute a series of operator functions such as map(), filter(), scan(), delay() and so on. A full reference for these RxJS operators is on the site. One such function id the debounce() function, which acts like a suppressor for observable sources. In the situation where we receive an undesirably frequent number of observables, we can limit the frequency at which we receive those observables before we can subscribe to the observable source.

The most common approach we see is to apply searches from text that is input directly into a form edit control. When a change event is triggered on the edit control and runs a query, the repetitive calls to an API can be inefficient, in many cases running whenever the user types in one character or is in the process of typing in a partial search term, the un-intended results are more queries run on partial search terms.

What the Debounce operator allows us to do is to delay triggering the change event for a set duration. This allows the user more time to complete a valid search term and trigger less unintended searches on our API. This is both a UI improvement for our end user and a performance improvement when calling a backend API or service.   A typical search HTML with edit control is shown:

<h2>Book Search</h2>
<mat-card>
    <mat-card-title>
    </mat-card-title>
    <mat-card-content>
        <mat-form-field class="example-full-width">
            <mat-label>Search Text</mat-label>
            <input class="form-control" [(ngModel)]="userQuery" 
            type="text" name="userQuery" id="userQuerys"
            (ngModelChange)=
               "this.searchQueryUpdate.next($event)" matInput>              
        </mat-form-field>

        <hr />

        <div *ngIf="hasSearchResults$ | async; else noRecords">
            <div style="display: flex; flex-wrap: wrap; border-style: solid; border-width: 1px; background-color: cadetblue;">
                <div style="flex: 0 50%;"><u><b>TITLE</b></u></div>
                <div style="flex: 0 50%;"><u><b>AUTHOR</b></u></div>
            </div> 

            <div style="display: flex; flex-wrap: wrap; border-style: solid; border-width: 1px;" *ngFor="let book of searchResults">
                <div style="flex: 0 50%;">{{book.title}}</div>
                <div style="flex: 0 50%;">{{book.author}}</div>
            </div> 
        </div>
        <ng-template #noRecords>
            Awaiting search results ...
        </ng-template>
    </mat-card-content>
</mat-card>

In the above Angular form template, we have an edit control which has a subscription to the component property searchQueryUpdate which initiates a subscription to call to filter out matching books.

searchQueryUpdate = new Subject<string>();

The code for running the filter on the data is shown:

getBooks()
{
  this.searchQueryUpdate.pipe()
    .subscribe(value => {
      this.searchResults = [];
      this.books.filter(b => b.title.includes(value))
        .forEach(b => {
          if (!this.searchResults.find(c => c.id === b.id))
            this.searchResults.push(b);
          this.hasSearchResults$.next(this.searchResults.length > 0);
        })
    });
}

Before we can filter the books, the data is loaded by running a HTTP GET API to query book data on form initiation:

ngOnInit() {
  this.api.getBooks().pipe(delay(3000))
    .subscribe((res: Book[]) => {
      this.books = res
      console.log("read books from API");
    });

    this.getBooks();
}

When we run the application the search page will look as shown, waiting for a search term to be entered.

The visibility of the search grid is shown with the following conditional HTML template:

<div *ngIf="hasSearchResults$ | async; else noRecords">
  <div style="display: flex; flex-wrap: wrap; border-style: solid; 
    border-width: 1px; background-color: cadetblue;">
    <div style="flex: 0 50%;"><u><b>TITLE</b></u></div>
    <div style="flex: 0 50%;"><u><b>AUTHOR</b></u></div>
  </div> 

  <div style="display: flex; flex-wrap: wrap; border-style: solid; 
      border-width: 1px;" *ngFor="let book of searchResults">
    <div style="flex: 0 50%;">{{book.title}}</div>
    <div style="flex: 0 50%;">{{book.author}}</div>
  </div> 
</div>
  
<ng-template #noRecords>
  Awaiting search results ...
</ng-template>

We control visibility of the search display using an asynchronous observable hasSearchResults$:

hasSearchResults$ = new BehaviorSubject<boolean>(false);

This observable starts with an initial false value and acts like a toggle to hide the results div and shows only the template when there is no incoming data through the observable source.

When there are search results the results div is displayed as shown:

Next, we replace part of the filtering code with the Debounce operator:

getBooks()
{
  this.searchQueryUpdate.pipe(
    debounceTime(400),
    distinctUntilChanged())
      .subscribe(value => {
        this.searchResults = [];
        this.books.filter(b => b.title.includes(value))
          .forEach(b => {
            if (!this.searchResults.find(c => c.id === b.id))
              this.searchResults.push(b);
            this.hasSearchResults$.next(
              this.searchResults.length > 0);
          })
  });
}

Notice the difference between the search filter we had before and now: We have moved the filter before subscribing to the source observable, which reduces the frequency of the number of keystrokes sent to the backend search.

After applying the change for the pre-subscription filter our UI search key entry will seem a little slower for the user, however the speed of interaction is offset by the selective efficiency of search result retrieval.  

The use of the RxJS Debounce filter operator is devised to achieve the three goals:

  1. Avoid unnecessary filters or queries being applied from UI event changes.
  2. Allow the user to enter more meaningful searches without excessive queries.
  3. Lower the application bandwidth when running queries and API calls.

That’s all for this post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial