Jędrzej Dudkiewicz
Jędrzej Dudkiewicz

Reputation: 1122

Why rxjs' mergeMap turns array into separate objects

I am not Typescript, Javascript or frontend developer at all. I have inherited Angular 13 project. There is a function returning Observable<Dto[]>. I see something along this lines in the code:

this.service.getStuff(arg).pipe(
    mergeMap(x => x),
    concatMap(y => { return this.service.getOtherStuff(x.subObj.id).pipe(
        map(v => ({ obj1: y, obj2: v})));
    })
).subscribe(
// ... do something with results

I understand what it does and why it does that, but I can't for the life of me understand why would mergeMap turn an array into a stream of separate values. Documentation says only that mergeMap:

Projects each source value to an Observable which is merged in the output Observable

My understanding is that source value in this case is Dto[], so projection should be Observable<Dto[]>, not a list of Observables created from separate values in the array.

I suspected that maybe this maneuver is done later, but putting tap with console.log around mergeMap shows that input is indeed an array and output is a stream objects.

So what is happening and why? And why isn't it explained in the documentation while we're at it.

Upvotes: 2

Views: 941

Answers (3)

Jędrzej Dudkiewicz
Jędrzej Dudkiewicz

Reputation: 1122

I have accepted @martin's answer as it allowed me to find an answer in the documentation - but not on https://rxjs.dev site but rather on https://reactivex.io site. As @martin pointed out, what project function should return is ObservableInput, which is defined as:

type ObservableInput<T> = Observable<T> | InteropObservable<T> | AsyncIterable<T> | PromiseLike<T> | ArrayLike<T> | Iterable<T> | ReadableStreamLike<T>;

Part ArrayLike<T> is the key and on reactivex.io we can read more (and I couldn't find this VERY important piece of info on https://rxjs.dev site), most importantly:

Array Arrays can be interpreted as observables that emit all values in array one by one, from left to right, and then complete immediately.

Array-like Arrays passed to operators do not have to be built-in JavaScript Arrays. They can be also, for example, arguments property available inside every function, DOM NodeList, or, actually, any object that has length property (which is a number) and stores values under non-negative (zero and up) integers.

This piece of information is of paramount importance and I can't believe it is not written in the docs on the other site.

Upvotes: 1

martin
martin

Reputation: 96891

This is intended behavior. If you look at the documnetation it says that the project function needs to return ObservableInput and not just Observable. In fact, all RxJS operators that expect an Observable as a parameter will work with any ObservableInput.

ObservableInput can be for example an Observable (obviously), async generator, an array or a Promise.

So using mergeMap(x => x) is really only used to unwrap the array and is equivalent to mergeMap(x => from(x)). Maybe more clean solution would be using just mergeAll() operator that is intended to be used with higher-order Observables.

Upvotes: 5

Tobias S.
Tobias S.

Reputation: 23825

The issue here is that you are using mergeMap not how it is normally intended. The map operators like mergeMap or switchMap expect an Observable (or a Promise) as the return value. But you are just returning the plain array. It seems like mergeMap sees that this is not an Observable and wraps it in a from operator. This creates an Observable from the array where each element emits individually.

If you do not want this behavior, wrap your value in of.

mergeMap(x => of(x))

But really this mergeMap does not have any effect either way. Why are you using this do-nothing function here? If you don't plan to return an Observable, why not use a map?

Upvotes: 2

Related Questions