„Nested subscriptions are bad programming style“. That is, what many programmers tell us. But: how can we get rid of nested subscriptions? In this little step-by-step tutorial, we will demonstrate how to use flatMap to improve an Angular 6 based REST API example. We will also discuss the difference between the flatMap and the switchMap function. We will show an example, where switchMap will fail badly, while flatMap works as expected.
If you want to learn the basics on RxJS, I recommend reading this nice beginner’s series about RxJS by Yakov Fain (look for the list of contents and the pingback section to find all posts of this series).
Angular REST API Example, Step 8.7: How to get rid of nested Subscriptions
In this subchapter, we will perform the following steps:
- show our starting point from a previous chapter of this document (chapter 8.7)
- discuss nested subscriptions,
- introduce flatMap as a possible solution
- discuss differences between flatMap and switchMap
- show how to replace nested subscriptions by a switchMap.
Interval Solution with Nested Subscriptions
Here we had developed solutions that perform periodic HTTP GET requests. In all shown variants, we have used nested subscriptions. Here is the simplest quick&dirty solution we had developed:
Note: the full code can be downloaded via GIT with
git clone https://github.com/oveits/consuming-a-restful-web-service-with-angular-vi git checkout feature/0009-refresh-via-interval-using-subscribes
src/app/app.component.ts
...
interval(10000).subscribe((counter) => {
this.appService.getAll().subscribe(posts => {this.posts = posts;});
...
});
...
In the HTML template, we had looped over the posts array:
src/app/app.component.html
...
<ul>
<li *ngFor="let post of posts" [innerHTML]="post.title" (click)="selectedPost = post">
</li>
</ul>
...
Nested Subscriptions are bad Programming Style
Yakov Fain points out in this blog post that nested subscriptions are bad programming style. But wait, we might have fixed that already: some variants in chapter 8 have used async pipes instead of subscriptions, right?
Interval Solution with chained async Pipes
Note: the full code can be downloaded via GIT with
git clone https://github.com/oveits/consuming-a-restful-web-service-with-angular-vi git checkout feature/0011-refresh-via-interval-using-async-pipes-fixed-via-class-variable-assignment
We had created a solution with a nested Observable, but no subscription was needed:
src/app/app.component.ts
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { AppService } from './app.service'; import { SafePost } from './safe-post.interface'; import { interval } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { interval_MSEC = 10000; postsVoid$$: Observable<Observable>; posts: SafePost[]; constructor(private appService: AppService) {} ngOnInit() { this.postsVoid$$ = this.getRestItemsIntervalVoid$$(); } getRestItemsIntervalVoid$$(): Observable<Observable> { return interval(this.interval_MSEC) .pipe( map( counter => { console.log(counter + ': read restItems'); return this.assignRestItemsToPosts$(); } ) ); } assignRestItemsToPosts$(): Observable { return this.appService.getAll().pipe( map(posts => { this.posts = posts; })); } }
src/app/app.component.html
In the HTML Template, we had used a chained async pipe in a hidden element for subscribing to the inner Observable:
...
<div hidden>{{postsVoid$$ | async | async}}</div>
...
So, did we get rid of the (evil!) nested solutions here? No, we did not. Why not? The reason to this is that chained async pipes in the HTML template represent nested subscriptions. Let us explain that a little bit:
A single async pipe in an HTML template corresponds to a subscription:
(.ts) myObservable$ .subscribe() <--> (.html) myObservable$ | async
Any Observable will tell us about its events only if we subscribe to it. And, obviously, an async pipe reveals such events to the viewer of the HTML page. Therefore, an async pipe is a subscription managed by the HTML template.
The same arguments lead us to the conclusion that chained async pipes are equivalent to nested subscriptions:
(.ts) myObservableOfAnotherObservable$$.subscribe().subscribe() <--> (.html) myObservableOfAnotherObservable$$ | async | async
Chained async pipes are equivalent to nested subscriptions
We also get the hint, when we look at the type of variable we subscribe to:
postsVoid$$: Observable<Observable<void>>;
postsVoid$$ is an Observable of an Observable. And the inner Observable will reveal its events only if we subscribe to the outer Observable as well as the inner Observable. As long as we cannot get rid of nested Observables, we also will not get rid of nested subscriptions, when we want to get access to the inner Observable’s events.
Do flatMaps help?
flatMap/mergeMap
As Yakov Fain points out here, we can use a flatMap (a.k.a. mergeMap) to get rid of nested subscriptions. In a marble presentation, this looks as follows:
Here, I have borrowed the picture from the ReactiveX flatMap documentation.
Note: The ReactiveX site offers good documentation on RxJs operators.
The round marbles in the upper line represent events of the outer observable. Each round marble event will create its own inner observable. The diamond-shaped events of the „red“, „green“ and „blue“ inner observables are colored correspondingly.
Now, a flatMap allows us to subscribe to all events of the sum of all inner observables with a single subscription.
Note: mergeMap is an alias to flatMap and has the same functionality.
How about switchMaps?
switchMap
Below, you see a visualization of the difference between flatMap and switchMap:
Once a new event is triggered by the outer observable, old inner observables are removed. In the above image, the blue outer event cancels the green inner observable and all of its future events.
switchMap is a flatMap with the difference that any new event of the outer event stream will cancel any previously created inner Observables and their subsequent events. The stream will „switch“ to the newly created inner event stream instead.
Getting rid of nested Subscriptions with a flatMap
To get rid of the nested solution above is easier than expected. Use the solution with the chained async pipes and replace the outer map with a flatMap function.
src/app/app.component.ts
import { Observable } from 'rxjs'; import { AppService } from './app.service'; import { interval } from 'rxjs'; import { map, flatMap } from 'rxjs/operators'; ... postsVoid$: Observable<void>; ... ngOnInit() { this.postsVoid$ = this.getRestItemsIntervalVoid$(); } getRestItemsIntervalVoid$(): Observable { return interval(this.interval_MSEC) .pipe( flatMap( counter => { console.log(counter + ': read restItems'); return this.assignRestItemsToPosts$(); } ) ); } ...
At the same time, we had to change the types from Observable<Observable<…>> to a single Observable<…>. Therefore, we have changed the names from $$ ending to $ ending to show that those are non-nested Observables.
src/app/app.component.html
In the HTML Template, we had used a chained async pipe for subscribing to the inner Observable. We now need to change this to a single async pipe of the (non-nested) Observable:
...
<div hidden>{{postsVoid$ | async}}</div>
...
That’s it! The functionality can be verified in a Chrome Browser by hitting F12:
Here, I have chosen a short interval of 1 sec.
The async pipe will subscribe to the inner Observable and therefore HTML GET requests will now be sent periodically again, as before. This time, we do not need chained async pipes, though. And, as we have pointed out above, avoiding chained async pipes means avoid hidden nested subscriptions.
Note, that using a switchMap can lead to poor results in our case if the timer is too low. When choosing a relatively short timer value of 1 sec, we will observe the following behavior:
The Blog posts will never show up, since the delay of the WordPress REST API is larger than one second, while we have chosen an interval timer of 1 sec. As described above, each new interval event (outer Observable) will cancel all previous HTTP GET requests. Since the response time of each HTTP GET requests exceeds 1 sec, the response will never reach the client in time, before the GET request is canceled.
This is the expected behavior in this situation. Using a switchMap is only recommended in situations, where an outer event is invalidating the inner events, so waiting for the inner event has no value to the user. Here, it makes perfect sense to wait for the response of the HTTP request, since the response is valuable: the information is still valid, presumably. Therefore, in such a situation, flatMap makes more sense.
Summary
In this blog post, we have discussed RxJS flatMap and switchMap. We have applied both functions to a solution that refreshes the value of a variable periodically from a REST API. We have found that flatMap works better than switchMap in situations, where the response delay of the HTTP GET is larger than the interval timer.
Note: the code of the working flatMap example can be downloaded with GIT like follows:
git clone https://github.com/oveits/consuming-a-restful-web-service-with-angular-vi git checkout feature/0011-2-refresh-via-interval-using-async-pipes-fixed-via-class-variable-assignment-with-flatmap
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.