Flattening map operators in RxJS

2022-09-2110 min
Flattening map operators in RxJS

Introduction

Hello, In this article I want to talk about RxJS Flattening map operators. This topic might be hard for Angular/RxJS newbies, but for sure understanding it is rewarding. If you are not familiar with Angular or RxJS at all, then no worries, you can still enjoy the text. Maybe it will convince you to try this library in your projects.

Flattening operators

During working with RxJS We often run into higher-order Observables. Higher-order Observables are simply speaking nested Observables. Let me give you and example. Let's consider fetching articles based on search by title.

   this.searchControl.valueChanges
     .subscribe(search => {
       this.articlesService.getArticles(search)
         .subscribe(articles => this.articles = articles)
     })

Above example of nested subscriptions is really common. This implementation will work, but for sure it looks ugly. The other downside is that with this approach We can't really use Push-based mechanisms(I already wrote about it in this article). So how can We handle it better? Well, typically by flattening (converting a higher-order Observable into an ordinary Observable).

In RxJS We have four flattening map operators:

  • mergeMap,
  • switchMap,
  • concatMap,
  • exhaustMap,

mergeMap

mergeMap is an operator which map value from outer Observable to inner Observable for every emission of outer Observable, without keeping order, so it doesn't wait untill previous inner observable completes. Let's see it on attached diagram.

mergeMap-diagram

As an example We will consider real use-case of this operator. Let's assume that We have an array of article ids, and for each id We want to fetch adequate article.

ids$ = from([1, 2, 3]);
articles$ = this.ids$.pipe(
    mergeMap(id => forkJoin({
      id: of(id),
      article: this.getArticle(id)
    })),
    buffer(this.ids$),
    last(),
 )

The result will be the following:

 [{ id: 1, article: {...} }, { id: 2, article: {...} }, { id: 3, article: {...} }]

But what actually is happening in this code?

  • from operator creates Observable which emits 1, 2, 3, one after another,
  • for every single emmision, mergeMap maps the value(1, 2 or 3) to new inner Observable which value is_ { id: {id}, article: {...} }_,
  • forkJoin let us group id with article in one object (for better accessibility),
  • buffer will collect the output values as an array when Observable completes,

Summing up, We use mergeMap operator when We want to map each value emmision of outer Observable to new inner Observable without keeping an order.

concatMap

concatMap operator is really similiar to mergeMap. The only difference is that concatMap keeps an order of emmision, it means that it waits for previous inner Observable to complete before mapping new one. We can easily compare it to queue. You can see it on attached diagram.

concatMap-diagram

The same result We can find in Network tab. Let's see how our inner Observables (articleService.getArticle() API calls) looks like for both operators.

mergeMap

mergeMap.jpg

concatMap

concatMap.jpg

switchMap

switchMap basically acts the same as concatMap and mergeMap in the context of flattening and mapping. The main difference is that switchMap provides cancelling effect. It means that if outer Observable emits new value at the same time as inner Observable is still processing(f.ex request is pending), it will cancel the current inner Observable async operation(request in this case) and proceed with new one. Let's see it on the diagram.

switchMap-diagram

Now I want to give you some real life example of switchMap usage. Let's consider the following scenario. We have a table of articles with couple of filters(title, content, author), pagination and sort. On every criteria change We want to fetch corresponding articles. This is how our code may look like:

articles$ = combineLatest([
    this.filters$,
    this.pagination$,
    this.sort$,
])
.pipe(
    switchMap(([filters, pagination, sort]) =>
        this.articlesService.getArticles(filters, pagination, sort)
    ),
);

Let's list what happens here:

  • first We combineLatest all three BehaviorSubjects, in the result it will emit the value for every criteria(BehaviorSubject) change,
  • second We map each outer Observable emmision using switchMap to inner Observable - API call,

Why did I use switchMap here? Well, We can assume that criteria may change a lot(user can search through titles, content, sort and use pagination). It can happen that criteria will change when the previous request to API will still be pending. Ofcourse We don't need articles for outdated search criteria. That's exactly the reason for using this operator here. For every new emission, if request will still be pending, it will be canceled and proceed with new one.

exhaustMap

exhaustMap works opposite to switchMap. For every new emmision of outer Observable, if inner Observable is still pending, the new emmision will be canceled. Let's check it on below diagram.

exhaustMap-diagram

So when should We use exhaustMap? For sure login functionality is a good example.

login$.pipe(
    exhaustMap(credentials => this.authService.login(credentials)),
)

Let's assume that login$ will emit new value for each Login button click. So how it will behave?

  • every click will be mapped to new inner Observable using exhaustMap,
  • if login request to API will still be pending, nothing will happen, click will be ignored and new request won't be send,

Now let's compare switchMap and exhaustMap behavior in Network tab.

switchMap

switchMap-network.jpg

1 and 2 was cancelled due to new emission of 3

exhaustMap

exhaustMap-network.jpg

2 and 3 were ignored due to pending request(1)

Conclusion

Flattening map operators are really powerful functions. It's important to have a good understanding how and when to use it. I hope that this article helped you and from now you will be successfully using it in your projects.