Learn RxJS switchMap, mergeMap, concatMap and exhaustMap, FOREVER! 😎

Last month I went to ng-conf 2018 together with Mike Brocchi and Pete Bacon Darwin to give a talk called:

“I switched a map and you’ll never guess what happened next…”

(if you want to read about the behind the scenes leading up to the event,
check out my last post about it).

Before you continue, watch our “how switchMap works” talk:

Pretty unusual huh? 😜

BTW, if you’re interested in the post talk “behind the scenes” stories- check out my personal story from this Adventures in Angular podcast interview and our live RETROSPECT post talk meeting chat of me, Mike and Pete on the Angular Air podcast.

Make sure you watch the talk before moving on👆

it will help you a lot with the rest of this post!

And now, for the super secret ninja Rx learning trick

(first, I want to say that this article is based on what me, Pete and Mike brainstormed together, I didn’t come up with the following realizations by myself)

So… after watching this lecture, you might think that you just learned
about “
switchMap” and that’s it

but in fact…

You ALSO learned…

how the other RxJS operators: mergeMap, concatMap and exhaustMap work without even noticing it!

“What? How is that even possible? you never mentioned “mergeMap” or the others even once!”

I know, right? 😊

The trick to learn and remember what all these operators do, is to realize that they are mostly the same, except for one thing.

This realization hit us during one of our online script writing sessions while preparing for this talk.

After we wrote 3/4 of the script, we realized that we actually wrote a more general explanation of how ALL the “mapping+flattening” operators work.

“Shai, what the hell is MAPPING + FLATTENING?”

OK, let’s break it down, shall we?

1. Mapping is easy, until…

When we map simple values, it’s super easy —

// Awesome Mappingconst namesObservable = of('Pete', 'Mike');namesObservable.pipe(   map(name => `${name} is awesome!`)).subscribe(result => console.log(`${result}`));// CONSOLE:
// Pete is awesome!
// Mike is awesome!

Pretty straight forward.

But things start to get confusing when we return an observable from the map function, and not just a simple value.

“Why would we want to return an Observable?”

For example, when we make http calls, or open up a web socket, we might want to return an observable of responses.

Let’s say we have a http object with agetAwesomeMessagesObservable method that takes a name and returns 2 messages from the “server” for that name:

// A FAKE SERVER OBJECT: const http = {   getAwesomeMessagesObservable(name): Observable<string> { 
return of(`${name} is awesome! (msg #1)`,
`${name} is awesome! (msg #2)`);
// the "of" returns an observable
// this particular one will produce 2 events
// Pete is awesome! (msg #1)
// Pete is awesome! (msg #2)

Then, when we want to connect it to our namesObservable to see all the awesome messages each person gets, we might try to do the following:

// Awesome ajax mappingnamesObservable.pipe(   map(name => http.getAwesomeMessagesObservable(name) )).subscribe(result => console.log(`${result}`));// CONSOLE:
// [object Object]
// [object Object]

But we fail. 😟

We fail because the result value in this case is what the getAwesomeMessagesObservable(name) method returns, and it is.. well…

an observable!

2. “Flattening” to the rescue!

Flattening just means — “subscribing inside a subscribe

The only way to extract values out of the returned observable will be to subscribe to it.

// Awesome ajax mappingnamesObservable.pipe(   map(name => http.getAwesomeMessagesObservable(name) )).subscribe(resultObservable => { 
resultObservable.subscribe(result => console.log(`${result}`) )
// Pete is awesome (msg #1)
// Pete is awesome (msg #2)
// Mike is awesome (msg #1)
// Mike is awesome (msg #2)

And without even realizing it, we just applied the “merge” strategy.

(Look at the console logs, notice how they are “merged” together)

“What is the merge strategy??? What are you talking about?”

Like we said in our talk:

whenever we flatten an Observable, we need a…

3. Flattening Strategy!

(whispering to myself: “flattening” 😊)

This is the design of the t-shirt I gave Pete after our talk.

Subscribing to an observable is just like adding a new addEventListener,
if you’re not careful you might cause memory leaks or run into weird bugs.

That’s why we need to make a decision of when we should subscribe or unsubscribe from the mapped (inner) observable.

Let’s do a short recap of what we have so far —

  1. We return an Observable from a map function.
  2. We “flatten” this Observable (subscribe to it)
  3. Now we need to decide Which strategy we are going to use.

There are 4 “Flattening Strategies

  • Merge” Strategy — deciding not to do anything, basically, we just keep subscribing to every new observable that we return from the map.
  • Switch” Strategy — unsubscribing from the last mapped observable, when the new one arrives.
  • Concat” Strategy —Queuing up every new Observable, and subscribing to a new observable only when the last observable completed.
  • Exhaust” strategy — the “don’t interrupt me” strategy, ignores (and never subscribe to) any new mapped Observable while the current Observable is still emitting values.

For each Flattening strategy RxJS has an operator that deals with it.

They come in a standalone version (i.e.mergeAll() ), that comes right after the map line in the pipe() chain.

map(name => http.getAwesomeMessagesObservable(name) ),

but they also come in a “Mapping + Flattening” operator (i.e mergeMap()), which saves you the extra map operator in your pipe() chain.

mergeMap(name => http.getAwesomeMessagesObservable(name) )

In this article we’ll discuss only the “Mapping + Flattening” operators.

There are 4 “MAPPING + FLATTENING” Operators

let’s call them “mappening” operators from now on 😀—

(1) mergeMap(), (2) switchMap()(3) concatMap() and (4) exhaustMap().

All of them mostly work in the same manner —

  1. They map some value to an observable (you are the one in charge of returning an observable value from them, they just map it)
  2. They flatten the observable you return ( they just subscribe to it)
  3. They decide about what to do before / after they flatten (“Flattening Strategy”)

And here is the NINJA TRICK I promised: The first 2 steps 👆 are identical to all 4 “Mappening” operators

Once you understand this, you’ll realize that you only need to decide which “flattening strategy” you want to apply, and then choose the appropriate operator for that strategy.

Let’s explore each one of these operators —

“mergeMap()” — the “Slacker” operator

Why do I call it the “Slacker” operator?

Because other than “mapping + flattening” the Observable, it doesn’t do anything else, its strategy is to have no strategy! 😛

Let’s take the example from our ng-conf talk:

In that talk, we had a CEO which gets a tweet about a new framework, then calls a recruitment agency and asks them to send recruits that are experienced in that framework.

(by the way, all of the following code examples could be found in this StackBlitz)

const frameworkTweetsObservable = from(['Backbone', 'Angular'])frameworkTweetsObservable.pipe(  map(framework => getAgency(framework) ),
map(agency => agency.getRecruitsObservable() )
).subscribe( recruitsObservable => { recruitsObservable.subscribe(recruit => console.log(recruit));

// this ^ is the important line, as this is the "merge" strategy
});// CONSOLE:
// Backbone Developer #1
// Backbone Developer #2
// Angular Developer #1
// Backbone Developer #3 (<-- still happening)
// Angular Developer #2

As we said, this is the merge strategy, we just keep subscribing to all the mapped recruits Observables.

So we could easily refactor this code to use the mergeMap operator:

const frameworkTweetsObservable = from(['Backbone', 'Angular'])
tap(fwName => console.log(`*** "${fwName}" tweet pops up ***`)
// don't mind this ^ logging code,
// we just use it to show in the console when a new tweet comes in
frameworkTweetsObservable.pipe( map(framework => getAgency(framework) ),
mergeMap(agency => agency.getRecruitsObservable() )
).subscribe( recruit => console.log(recruit) );// CONSOLE:
// *** "Backbone" tweet pops up ***
// Backbone Developer #1
// Backbone Developer #2
// *** "Angular" tweet pops up ***
// Angular Developer #1
// Backbone Developer #3 (<-- still happening)
// Angular Developer #2

Notice how mergeMap() is doing both the mapping and the flattening at the same time.

Real life example for mergeMap:

Enhancing search results

Let’s say you show a live list of movies (via push notifications, like netflix).

For each movie, you might want to also call a 3rd party service, like IMDB to show it’s “up to date” movie rank.

So you can mergeMap the movie into an http request to IMDB and by that, to enhance your movie cards with this additional data.

Now, let’s go back to our story…

This is great and all, but in the context of our story, it’s not what our CEO needs.

“What’s wrong with mergeMap?”

I’m not saying it is wrong, if that’s what you’re intending to do in your code.

But in our story, Kevin, the CEO (that’s me 😁), was just interested in getting only the developers who knew how to code in the latest technology.

If we used this merge strategy, it would have caused a lot of unnecessary frustration for Kevin, because he would keep getting ALL recruits for ALL the frameworks, at the same time.

To solve this problem, RxJS introduced the following operator…

switchMap() — the “Fashion” operator

With switchMap() we only care about the latest and greatest, AKA the hot new fashion. We really don’t care about the past.

This is what our CEO wanted- to stop getting Backbone recruits arriving to his front door as soon as he learned about a new technology.

const frameworkTweetsObservable = from(['Backbone', 'Angular'])
tap(fwName => console.log(`*** "${fwName}" tweet pops up ***`)
frameworkTweetsObservable.pipe( map(framework => getAgency(framework) ),
switchMap(agency => agency.getRecruitsObservable() )
).subscribe( recruit => console.log(recruit) );// CONSOLE:
// *** "Backbone" tweet pops up ***
// Backbone Developer #1
// *** "Angular" tweet pops up ***
// Angular Developer #1
// Angular Developer #2
// Angular Developer #3

Awesome, now Kevin is happy! 👏

Notice how the Backbone developers stop arriving right after the Angular tweet pops up.

Real life example for switchMap:


If you google something, you press a key on the big input box, and then you get suggestions for things you might mean to write.

So every new input triggers a new ajax request for that search term.

If you just used mergeMap you’ll get suggestions for every key stroke
(“m”, “my “, “my p” …. “my parrot is looking at me like I owe him money!”)

But switchMap will make sure that the ongoing http request is being canceled on every new search input, and only the newest http request is live.

But WAIT! There’s more…

There are 2 more “Mappening” operators we could have used —

concatMap() — the “Wait in line” operator

If Kevin, our CEO, was polite (and had tons of spare time), he could have chosen to wait until he finished all the interviews with the Backbone developers, before he started interviewing the Angular developers.

Even if he saw that Angular was the hot new framework, he would have waited until there were no more Backbone developers to interview.

Something like this:
(check out the console logs)

const frameworkTweetsObservable = from(['Backbone', 'Angular'])
tap(fwName => console.log(`*** "${fwName}" tweet pops up ***`)
frameworkTweetsObservable.pipe( map(framework => getAgency(framework) ),
concatMap(agency => agency.getRecruitsObservable() )
).subscribe( recruit => console.log(recruit) );// CONSOLE:
// *** "Backbone" tweet pops up ***
// Backbone Developer #1
// *** "Angular" tweet pops up ***
// Backbone Developer #2
// Backbone Developer #3 <-- this is the last one
// Angular Developer #1 <-- only now the Angular devs appear
// Angular Developer #2
// Angular Developer #3

So concatMap() queues up the inner observables one after the other, and play their events in that order (subscribing to the next Observable in the queue, only when the last one is completed).

Real life example for concatMap:

Top Ten List

Let’s say you have a live observable that emits a list of the top ten movies of all times.

For each movie, we do the same thing as the mergeMap and issue another ajax request to IMDB to get his rank, but this time, the order of display matters.

So by using concatMap we can block the next movie from finding out its IMDB rank until our current movie found out its rank, that way, we keep the original order of movies.

exhaustMap() — the “Do not disturb!” operator

So.. exhaustMap() … what is it?

Let’s say Kevin popped in a few pills of Methylphenidate ( Ritalin ), and all of a sudden got new SUPER FOCUS ABILITIES.

Meaning, that between Backbone interviews, he would completely ignore any new Framework tweet that came in his way.

He would see new tweets, but decide not to do anything about them, he even completely forgot about them (they were gone forever, not saved in some queue like in the concatMap() example).

Only after his backbone recruits interviews were done, then he could be open to listening to any new tweet and do something about it.

So it might have looked something like the following —

const frameworkTweetsObservable = from(['Backbone', 'Angular'])
tap(fwName => console.log(`*** "${fwName}" tweet pops up ***`)
frameworkTweetsObservable.pipe( map(framework => getAgency(framework) ),
exhaustMap(agency => agency.getRecruitsObservable() )
).subscribe( recruit => console.log(recruit) );// CONSOLE:
// *** "Backbone" tweet pops up ***
// Backbone Developer #1
// *** "Angular" tweet pops up *** <-- is completely ignored
// Backbone Developer #2
// Backbone Developer #3 <-- this is the last one
// (we never see any Angular developers, because the "Angular" tweet
// popped while the backbone observable was still alive)

As you can see, the Angular recruits observable never even gets stored for later use, there is no queue!

Real life example for exhaustMap:

Login screen

Let’s say you have a login screen with a login button, where you map each click to an login ajax request.

If the user clicks more than once on the login button, it will cause multiple calls to the server, and you probably don’t want that…

So you can use exhaustMap to temporarily “disable” the mapping while the first http request is still on the go — this makes sure you never call the server while the current request is running.

And this concludes all 4 “Mappening” operators!

So you see, that all 4 operators acts the same for the first 2 steps —

  1. They all map
  2. They all flatten

But the difference is when they need to decide how to handle the observable they get from the mapping operation.

So always remember —

when you need to map something to an observable, you also need to choose which flattening strategy to use.


When NOT to use these “Mappening” operators:

  1. If you’re not mapping anything, you have no reason to use any of them.

2. If you ARE mapping, but not returning an Observable from the map function, you have no reason to use them.

BONUS: All the code, running in your browser

You can see all the running examples (from the talk and from this blog post)
in this following Stackblitz project

(make sure you open the console of the preview window to see it running):

That’s it!

Again, I want to give credit to Pete Bacon Darwin & Mike Brocchi.
First — for being awesome and second — because this blog post is based on the findings and realizations of all three of us.

Plus, I want to thank Uri Shaked, Gil Fink and others for reviewing this article.

Hope you enjoyed the talk and this post, and most importantly — learned from them!

I’d love to hear what you think, so please write it in the comments below.

Reactive Teacher Man
@shai_reznik | @hirez_io

Watch 👇 my Angular courses (especially on Testing):


Please share — what did you think?

Did you get it? was it helpful? will you remember what all the “mappening” operators do? is there anything we could have done better?


Developer, Entertainer, Crazy person, Founder & teacher of the Angulars at HiRez.io.