Let's look at the difference between mergeMap and map
A Quick Aside That Will Come in Handy Later
Here's a custom observable that emits 2 random numbers:
function getCustomeObservable(min, max){
console.log(`Hello From getCustomObservable(${min},${max})`);
const getRndInteger = () =>
Math.floor(Math.random() * (max - min + 1) ) + min;
return new Observable(observer => {
console.log(`Hello From The Observable created by getCustomObservable(${min},${max})`);
for(let i = 0; i < 2; i++){
observer.next(getRndInteger())
}
});
}
Lets I have a bunch of tuples that I want to add together in a stream, then subscibe and print the added up numbers to the console. That might look like this:
const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const added$ = tuples$.pipe(
map(([a,b]) => a + b)
)
added$.subscribe(console.log); // 7, 11, 15
This prints out 7, 11, and 15. As you can see, map(lambda) is taking the observable tuples$ as input and returning the observable added$ as output. (pipe isn't really composing anything in this case so we'll just ignore it for this explanation). How does map know how to turn one stream into the other? It just applies the lambda to each value in the stream.
Okay, so what does this do?
const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
map(([a,b]) => getCustomeObservable(a, b))
)
rando$.subscribe(console.log);
The output will look something like this (depends on your browser log):
Observable: {isScalar: false}
Observable: {isScalar: false}
Observable: {isScalar: false}
This makes sense. Every tuple is used to create an observable. So the values in the rando$ stream are observables. Actually, our output is a bit different, it will look like this because our custom observable has some console logging too:
Hello From getCustomObservable(2,5)
Observable: {isScalar: false}
Hello From getCustomObservable(4,7)
Observable: {isScalar: false}
Hello From getCustomObservable(6,9)
Observable: {isScalar: false}
What you'll notice is that our observables are never run. The console message "Hello From The Observable created by [...]" never prints to the console. This is because we've created a bunch of observables, but we've never subscribed to them. How do we subscribe to them? Well, there are many ways actually.
const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
map(([a,b]) => getCustomeObservable(a, b)),
mergeAll()
)
rando$.subscribe(console.log);
mergeAll() expects the values of the stream to be observables. Which we could see the output of map(lambda) was creating. It then subscribes to all the incoming observables (running them) and merges their output.
Console:
Hello From getCustomObservable(2,5)
Hello From The Observable created by getCustomObservable(2,5)
4
3
Hello From getCustomObservable(4,7)
Hello From The Observable created by getCustomObservable(4,7)
6
7
Hello From getCustomObservable(6,9)
Hello From The Observable created by getCustomObservable(6,9)
7
8
This does the same thing:
const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
mergeMap(([a,b]) => getCustomeObservable(a, b))
)
rando$.subscribe(console.log);
Unlike map, mergeMap expects its lambda to return an observable. mergeMap subscribes to that observable and creates a stream with the outputs of that observable. When new values arrive, it expects more observables and subscribes to those, and merges their outputs together.
The console output is identical in this case to how it was with mergeAll
Here's another way to merge streams, though it'll have a slightly different order to the console output.
const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
map(([a,b]) => getCustomeObservable(a, b)),
toArray(),
mergeMap(arrayOfObservables => merge(...arrayOfObservables))
)
rando$.subscribe(console.log);
console output:
Hello From getCustomObservable(2,5)
Hello From getCustomObservable(4,7)
Hello From getCustomObservable(6,9)
Hello From The Observable created by getCustomObservable(2,5)
2
3
Hello From The Observable created by getCustomObservable(4,7)
7
4
Hello From The Observable created by getCustomObservable(6,9)
9
8
This time we collected all the observables in an array (toArray), then we merged the array of observables into a single observable with merge and then we subscribed to that single merged observable with mergeMap.
You'll notice in the console output that all three observables are created before the first one is run (subscribed to). RxJS observables do not generate values until subscribed to.
TlDr:
It is the merge part of mergeMap that makes it different from map. Merge, concat, exhaust, race, zip, switch, ect are all special ways of subscribing to a list of observables.
Aside For an RxJS Antipattern
const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
map(([a,b]) => getCustomeObservable(a, b)),
)
rando$.subscribe(val$ =>
val$.subscribe(console.log)
);
This is a way to merge your streams without using merge, mergeMap, or mergeAll. Oftentimes, when you're first learning RxJS, this feels like an intuitive approach. You're used to getting the output of a stream with a call to .subscribe(LAMBDA).
Nesting subscriptions is, however, an antipattern for good reason. "Oh, the tangled webs you'll weave!".
The biggest problem is that nested subscriptions are very close to merging, but pretty far from the other joining operators like concat, zip, exhaust, ect. Having all the joining operators used similarly makes a world of difference when maintaining/extending code.
The next is that streams can be multicasted, shared, replayed, ect - none of the logic found within a subscription can be part of that process. So data-processing steps are well served to remain within operators (custom or otherwise).