First of all, I would like to start off by saying that this isn't the first blog post about this subject. But what I miss the most in other posts is visualizing these operators. This is the reason I created this blog post. So off we go!
mergeMap
, switchMap
, concatMap
, and exhaustMap
are all called higher-order mapping operators. Instead of mapping values to other values, they map them to Observables, on which you can pipe further.
One of the most used ones is probably the switchMap
operator. Most commonly, it is used together with HTTP GET calls, something like this:
this.idParam$.pipe(
switchMap((id: number) =>
this.productsService.getProduct(id)
)
);
People know this operator because of its cancellation effect. But there's also the mergeMap
, concatMap
and even exhaustMap
operators, of which many people are not aware of or don't know when to use them. Let's go through them one by one.
Note: In each example image down below, you can see "SOURCE" and "INNER". "SOURCE" means the source Observable (e.g., a click event, ActivatedRoute query params, etc.), and "INNER" is the inner Observable that is returned by one of the higher-order mapping operators.
MergeMap
The mergeMap
operator is the easiest of them all. It will map each source value to an Observable, which is merged into the output Observable. Simply explained: whatever comes in, comes out, regardless of order.
In this example, you see that when the source (A
) gets called, the inner source starts emitting its values. When the source gets called again while the inner source is still emitting its values, the new values will be emitted as well. So if the source is called multiple times, all inner sources will emit their values in parallel.
Use case
A good use case of the mergeMap
is a list of items in a shopping cart. You have an overview of all items, and next to each item is a button to delete it from the cart. If you're in a hurry, you can delete several items at once by clicking each "delete" button quickly. As a customer, you expect that each item that you click to delete will effectively be deleted. And that is what the mergeMap will do; it will continue to delete each one, even if you already clicked one for deletion.
Now imagine you use switchMap
instead: you hastily click each "delete" button. Since the first delete is still ongoing on the server while you click the second delete, the first delete will be cancelled, but while the second delete is ongoing, the third is clicked for deletion, canceling the second one, etc. This makes deleting items in your shopping cart quite unpredictable.
SwitchMap
switchMap
will map each source value to an Observable, which is merged into the output Observable, but also cancel any previous inner Observable.
As you can see in the example, each time the source A
gets called, the emitting of the previous inner source will get cancelled. The new values will be emitted instead, unless source A
gets called again.
The cancellation effect is more visible when you call the source A
multiple times after each other:
Use case
As stated before, the switchMap
is most often used together with HTTP GET calls in order to cancel a previous call when a new one is requested soon after. For example, a "refresh" button that will refresh the data in a list. You could spam the button to refresh, but with switchMap
that would keep canceling all previous clicks.
Another good use case is autocomplete. While typing, the results are being fetched for you. But when the first results come in, it might be that you have already typed some more, which might return a more narrow result. So in that case, it would be handy that the previous call was cancelled to go for the newest one. The best practice is to combine your switchMap
with a debounceTime
operator to further improve your autocomplete.
ConcatMap
concatMap
will map each source value to an Observable, which is merged into the output Observable in a serialized fashion, waiting for each one to complete before merging the next. So, simply put, they emit values while maintaining order.
In the example, when the button is clicked for the first time, the inner source starts emitting its values. While it's still emitting, we call the source a second time. We can see that instead of emitting new values for the inner source, it waits until the values of the first time are all emitted. Hence, the order is maintained.
Use case
concatMap
is used where order is of importance. A use case could be a character builder in a game where you can choose several options, but some options are dependent on each other. For example, you choose a red hair color and, at the same time, want to add a moustache. The order is important here because you want a red moustache to be added. So the first API call will be with the "red hair" filter, followed by a second API call with the "moustache" filter. Due to the first API call, the second one already knows the hair color is red and can send over a red moustache.
Compare concatMap
with customers at a pharmacy. They all get served in the order when they arrived. Even if some arrive at the same time, they are handled in a sequential manner.
ExhaustMap
exhaustMap
will map each source value to an Observable, which is merged into the output observable only if the previous projected Observable has completed. In simpler terms, the new values are only emitted when the previous ones have all been emitted.
You can remember exhaustMap
by thinking that you can't make any new calls because it's exhausted from still emitting.
In the example, you see that the source is called multiple times. Only for the first call are the inner source values emitted (the red ones). All subsequent source calls are ignored, except the last call, since all previous inner values have been emitted.
Use case
A good use case would be to prevent double submissions. If a user keeps spamming the submit button, each call will go through. Using exhaustMap
will ignore any subsequent calls. You could use switchMap
, but that would mean that the submission would only go through when the user stopped spamming that submit button.
You could compare it to a restaurant. You give your order to the waiter, and that will make the kitchen start preparing your order. If you call the waiter again with the same order while they're still busy with your current order in the kitchen, it will not make the kitchen start you on a new order. No, they will prepare it, and when it is ready, you can call the waiter again with that same order.
Airport luggage handling analogy
Let's compare all these operators with the handling of luggage at an airport. At this airport, there is only one conveyor belt to handle luggage. When an airplane arrives, all luggage is put on the conveyor belt so each traveler can retrieve their luggage.
With mergeMap
When an airplane arrives, the luggage is put on the conveyor belt, one after the other. When another airplane arrives as well, the luggage of that one is also put on the conveyor belt, merged with the luggage of the other airplane. So the conveyor belt will show a mix of luggage from the first and second airplane.
With switchMap
When an airplane arrives, the luggage is put on the conveyor belt, one after the other. But as soon as another airplane arrives, they stop putting any luggage from the previous airplane on the conveyor belt. They cancel any further luggage handling for that airplane and begin handling the luggage for the newly arrived airplane.
With concatMap
When an airplane arrives, the luggage is put on the conveyor belt, one after the other. When a new airplane arrives, all the luggage from the first airplane is put on the conveyor belt. Only when all that luggage is handled will the handling of the luggage on the second airplane start.
With exhaustMap
When an airplane arrives, the luggage is put on the conveyor belt, one after the other. When a new airplane arrives and the luggage of the first airplane is still handled, the luggage handling personnel simply ignores the second airplane. The luggage on the second airplane will not be handled.
TLDR;
-
mergeMap
: come in, merge, come out. -
switchMap
: come in, merge, come out, but cancel when new ones come in. -
concatMap
: come in, merge, come out, but maintain order. -
exhaustMap
: come in, merge, come out, but while ongoing, ignore new ones when they come in.
If you're having trouble choosing the correct higher-order mapping operator, have a look at the following decision tree. It is based on the operator decision tree at RxJS.dev.
Please feel free to play around in the embedded StackBlitz down below. The example images on this blog are based on this StackBlitz.
If you have any questions, feel free to contact me!
Top comments (4)
Awesome Explanation! Thank you.
The Best..!!!
Great explanation. Loved the airport analogy!!
Great Explaination! From many of the post, I can understand from your post