User interface
Angular Asynchronous RxJS SPA Typescript

Using RxJS SwitchMap to Control Observable Flow in Angular Components

Welcome to today’s post.

In today’s post I will be discussing what the SwitchMap() function is in RxJS and how it can be used in an Angular component to control the flow of observables from user interaction to resulting output.

In previous posts I discussed how to use other RxJS functions such as CombineLatest() to control data synchronization, showed how to use BehaviorSubject() to synchronize data loading, and showed how use Debounce() to filter results in incremental searches.

What I will be discussing is how the SwitchMap() function can be applied to an observable to end a previous observable and start a new observable. In what cases would the switching of mappings be beneficial? When the number of observables emitted from the corresponding outer observable is quite frequent and we are not concerned about losing values in the stream of observables before they reach the inner observable, then switching to an inner observable and cancelling any incoming outer observables is beneficial as it will prevent excessive processing of subscriptions of inner observables. This is what we did for the debouncing effect where we cancelled excess keystrokes in an incremental search. When we wish to preserve all the outer observables before they are mapped to inner observables and have concurrent inner subscriptions, we could use a MergeMap(), which would process all the outer observables sequentially. A merge map is ideal when we wish to process all the emitted outer observables into a transaction.

An Example of using SwitchMap in a Filtered Search

To use SwitchMap() we import from the RxJS operators library as shown:

import { delay, switchMap } from 'rxjs/operators';

Suppose we have a component that has two sub-components:

  • A tiled list of genres that are selectable.
  • A list of books that are returned from a service.

When we select one of the genres, the books are filtered by the genre. Our initial screen has no selected genres, so it looks as shown:

I will show the code used to achieve the above user interaction.

Variables storing the observables is shown below:

 genres = [];
 selectedGenre: string;
 genre$: Subscription;
  
 booksList$: Observable<Book[]>;
 selectedGenre$ = new Subject<string>();

We have two observables, bookList$ and selectedGenres$

To initialize the observables, we do this within the component initialization:

ngOnInit() {
    this.selectedGenre = "Fiction";

    console.log("BookNewComponent instantiated");
        
    this.genre$ = this.lookupService.getGenres().subscribe(
        genres => {
            if (!genres)
                return;
            console.log("number of genres  = " + genres.length);
      
            this.genres = genres.map(g => g.genre);
            this.selectedGenre = this.genres[0];
            this.selectedGenre = this.selectedGenre.charAt(0).toUpperCase() + 
            this.selectedGenre.slice(1);
            this.selectedGenre$.next(this.selectedGenre);
        }
    );

    this.booksList$ = this.api.getBooks().pipe(delay(this.apidelay));
    ..

In the next section, I will show how to take the asynchronous observables for the genre selection and the books API call, pipe them into switch maps, the transform the resulting observables into a view of the books that filters out the selected genre.

Piping Observables into a SwitchMap

We will then pipe the above outer observables to the switch map and process their inner observables, combining their subscriptions, then filtering and transforming their data as shown:

combineLatest(
    this.booksList$.pipe(
        switchMap((b: Book[]) => of(b))
    ), 
    this.selectedGenre$.pipe(
        switchMap((g: string) => of(g))
    )               
)
.subscribe(([books, genre]) => {
    if (!books)
        return;
    if (!genre)
        return;
    this.hasLoaded$.next(true);
    this.books = books.map(b => {
        if (b.genre)
            b.genre = b.genre.toLowerCase();
        return b;
    });
    this.bookView = this.books.filter(b => b.genre === genre.toLowerCase());
    this.bookView.map(b =>
    {
        b.isCollapsed = true;
        b.isLoanCollapsed = true;
        b.loans = []; 
    });
    this.numberBooks = this.bookView.length;
    console.log("read books from API");
});

When the observables have been setup and the application run, the tiles when selected, will trigger the following event which sets the current genre and the emits the next value for selected genre observable:

eventSelection(value: any){
    this.selectedGenre = value;
    this.selectedGenre$.next(value);
}

After both observables are subscribed and both are non-empty, the corresponding HTML will display the book list when the hasLoaded$ async observable is set:

<mat-card-content>
    <div>
        <label class="boldedLabel" for="selectGenre1">Genre:</label>
        <br />
        <app-single-select-group 
            [availableItems]="genres" 
            [selectedItem]="selectedGenre" 
            (selectionChange)="eventSelection($event)">
        </app-single-select-group>
    </div>

    <div *ngIf="hasLoaded$ | async; else loading">
        <div style="display: flex; flex-wrap: wrap; border-style: solid; 
            border-width: 1px; background-color: cadetblue;">
            <div style="flex: 0 5%;"><u><strong>&nbsp;</strong></u></div>
            <div style="flex: 0 40%;"><u><strong>TITLE</strong></u></div>
            <div style="flex: 0 35%;"><u><strong>AUTHOR</strong></u></div>
            <div style="flex: 0 20%;"><u><strong>&nbsp;</strong></u></div>                        
        </div>
 
        <mat-list *ngFor="let book of bookView">
            <div style="display: flex; flex-wrap: wrap; 
                 border-style: solid; border-width: 1px;">
                 <div class="grid-row" style="flex: 0 5%;" 
                     class="clickLink" (click)="expandBookRecord(book)"> 
                     {{book.isCollapsed ? '+' : '&ndash;'}}
                 </div>
                 <div class="grid-row" style="flex: 0 40%;">
                     {{book.title}}
                 </div>
                 <div class="grid-row" 
                     style="flex: 0 35%;">
                     {{book.author}}
                 </div>
                 <div class="grid-row" style="flex: 0 20%;" 
                     class="clickLink" 
                     (click)="selectBookById(book.id)">
                     <u>VIEW</u>
                 </div>
            </div> 
        </mat-list>

        <app-pagination 
            [itemsPerPage]="10" 
            [totalItens]="numberBooks"
            [dataSource]="bookView"
            (pageChange)="eventPageChanged($event)"
            (pagedOutputChange)="eventPagedOutputChanged($event)">
        </app-pagination>
    </div>
    <br />
    <ng-template #loading>
        <div>
            <br />
            <mat-progress-spinner
                class="example-margin"
                [color]="primary"
                [mode]="mode"
                [value]="value">
            </mat-progress-spinner>
            loading books .. please wait
        </div>
    </ng-template>
</mat-card-content>

When the book list results and the genre both have valid data, the screen will show the books filtered by the selected genre as shown:

Once the inner observable has been subscribed, it is disposed of.

With switched maps we have controlled the flow of outer observables, flattened those to inner observables, and disposed of inner observables once then have been subscribed.

 That is all for today’s post. I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial