Rxjs Higher Order Mapping
Rxjs Higher Order Mapping
For example, most of the network calls in our program are going to
be done using one of these operators, so getting familiar with them
is essential in order to write almost any reactive program.
In the end, you will know exactly how each of these mapping
operators work, when to use each and why, and the reason for their
names.
Table of Contents
In this post, we will cover the following topics:
Observable Concatenation
Observable Merging
Observable Switching
The RxJs switchMap Operator
Conclusions
Note that this post is part of our ongoing RxJs Series. So without
further ado, let's get started with our RxJs mapping operators deep
dive!
As the names of the operators imply, they are doing some sort of
mapping: but what is exactly getting mapped? Let's have a look at
the marble diagram of the RxJs Map operator first:
How the base Map Operator works
With the map operator, we can take an input stream (with values 1,
2, 3), and from it, we can create a derived mapped output stream
(with values 10, 20, 30).
So the map operator is all about mapping the values of the input
observable. Here is an example of how we would use it to handle
an HTTP request:
1
2 const http$ : Observable<Course[]> = this.http.get('/api/courses');
3
4 http$
5 .pipe(
6 tap(() => console.log('HTTP request executed')),
7 map(res => Object.values(res['payload']))
8 )
9 .subscribe(
10 courses => console.log("courses", courses)
11 );
12
Now that we have reviewed how base mapping works, let's now
talk about higher-order mapping.
1
2 @Component({
3 selector: 'course-dialog',
4 templateUrl: './course-dialog.component.html'
5 })
6 export class CourseDialogComponent implements AfterViewInit {
7
8 form: FormGroup;
9 course:Course;
10
11 @ViewChild('saveButton') saveButton: ElementRef;
12
13 constructor(
14 private fb: FormBuilder,
15 private dialogRef: MatDialogRef<CourseDialogComponent>,
16 @Inject(MAT_DIALOG_DATA) course:Course) {
17
18 this.course = course;
19
20 this.form = fb.group({
21 description: [course.description, Validators.required],
22 category: [course.category, Validators.required],
23 releasedAt: [moment(), Validators.required],
24 longDescription: [course.longDescription,Validators.required
25 });
26 }
27 }
28
29
We could try to do all of this manually, but then we would fall in the
nested subscribes anti-pattern:
1
2 this.form.valueChanges
3 .subscribe(
4 formValue => {
5
6 const httpPost$ = this.http.put(`/api/course/${courseId}`,
7
8 httpPost$.subscribe(
9 res => ... handle successful save ...
10 err => ... handle save error ...
11 );
12
13 }
14 );
15
As we can see, this would cause our code to nest at multiple levels
quite quickly, which was one of the problems that we were trying to
avoid while using RxJs in the first place.
Before exploring each one of these use cases, let's go back to the
nested subscribes code above.
In the nested subscribes example, we are actually triggering the
save operations in parallel, which is not what we want because
there is no strong guarantee that the backend will handle the saves
sequentially and that the last valid form value is indeed the one
stored on the backend.
Let's see what it would take to ensure that a save request is done
only after the previous save is completed.
Understanding Observable
Concatenation
In order to implement sequential saves, we are going to introduce
the new notion of Observable concatenation. In this code example,
we are concatenating two example observables using the
concat() RxJs function:
1
2 const series1$ = of('a', 'b');
3
4 const series2$ = of('x', 'y');
5
6 const result$ = concat(series1$, series2$);
7
8 result$.subscribe(console.log);
9
a
b
x
y
The of() function will create Observables that emit values passed
to of() and then it will complete the Observables after all values
are emitted.
in the output
1
2 this.form.valueChanges
3 .pipe(
4 concatMap(formValue => this.http.put(`/api/course/${courseId}`
5 )
6 .subscribe(
7 saveResult => ... handle successful save ...,
8 err => ... handle save error ...
9 );
10
Observable Merging
Applying Observable concatenation to a series of HTTP save
operations seems like a good way to ensure that the saves happen
in the intended order.
But there are other situations where we would like to instead run
things in parallel, without waiting for the previous inner Observable
to complete.
1
2 const series1$ = interval(1000).pipe(map(val => val*10));
3
4 const series2$ = interval(1000).pipe(map(val => val*100));
5
6 const result$ = merge(series1$, series2$);
7
8 result$.subscribe(console.log);
9
0
0
10
100
20
200
30
300
Now that we understand the merge strategy, let's see how it how it
can be used in the context of higher-order Observable mapping.
Let's now say that the user interacts with the form and starts
inputting data rather quickly. In that case, we would now see
multiple save requests running in parallel in the network log:
Observable Switching
Let's now talk about another Observable combination strategy:
switching. The notion of switching is closer to merging than to
concatenation, in the sense that we don't wait for any Observable
to terminate.
1
2 const searchText$: Observable<string> = fromEvent<any>(this.input.nativeElement
3 .pipe(
4 map(event => event.target.value),
5 startWith('')
6 )
7 .subscribe(console.log);
8
9
1
2 const searchText$: Observable<string> = fromEvent<any>(this.input.nativeElement
3 .pipe(
4 map(event => event.target.value),
5 startWith(''),
6 debounceTime(400)
7 )
8 .subscribe(console.log);
9
10
With the use of this operator, if the user types at a normal speed,
we now have only one value in the output of searchText$ :
Hello World
This is already much better than what we had before, now a value
will only be emitted if its stable for at least 400ms!
He
Hell
Hello World
Also, the user could type a value, hit backspace and type it again,
which might lead to duplicate search values. We can prevent the
occurrence of duplicate searches by adding the
distinctUntilChanged operator.
And that is exactly what the switchMap operator will do! Here is
the final implementation of our Typeahead logic that uses it:
1
2 const searchText$: Observable<string> = fromEvent<any>(this.input.nativeElement
3 .pipe(
4 map(event => event.target.value),
5 startWith(''),
6 debounceTime(400),
7 distinctUntilChanged()
8 );
9
10 const lessons$: Observable<Lesson[]> = searchText$
11 .pipe(
12 switchMap(search => this.loadLessons(search))
13 )
14 .subscribe();
15
16 function loadLessons(search:string): Observable<Lesson[]> {
17
18 const params = new HttpParams().set('search', search);
19
20 return this.http.get(`/api/lessons/${coursesId}`, {params});
21 }
22
1
2 fromEvent(this.saveButton.nativeElement, 'click')
3 .pipe(
4 concatMap(() => this.saveCourse(this.form.value))
5 )
6 .subscribe();
7
This ensures the saves are done in sequence, but what happens
now if the user clicks the save button multiple times? Here is what
we will see in the network log:
the values g-h-i of the third Observable will then show up in the
output of the result Observable, unlike to values d-e-f that are
not present in the output
Just like the case of concat, merge and switch, we can now
apply the exhaust strategy in the context of higher-order
mapping.
1
2 fromEvent(this.saveButton.nativeElement, 'click')
3 .pipe(
4 exhaustMap(() => this.saveCourse(this.form.value))
5 )
6 .subscribe();
7
If we now click save let's say 5 times in a row, we are going to get
the following network log:
As we can see, the clicks that we made while a save request was
still ongoing where ignored, as expected!
This repository includes a small HTTP backend that will help to try
out the RxJs mapping operators in a more realistic scenario, and
includes running examples like the draft form pre-save, a
typeahead, subjects and examples of components written in
Reactive style:
Conclusions
As we have seen, the RxJs higher-order mapping operators are
essential for doing some very common operations in reactive
programming, like network calls.
Choosing the right operator is all about choosing the right inner
Observable combination strategy. Choosing the wrong operator
often does not result in an immediatelly broken program, but it
might lead to some hard to troubleshoot issues over time.
I hope that you have enjoyed this post, if you are looking to learn
more about RxJs, you might want to check out our other RxJs
posts in the RxJs Series.