2

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.

Jędrzej Dudkiewicz
  • 1,053
  • 8
  • 21

3 Answers3

4

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.

martin
  • 93,354
  • 25
  • 191
  • 226
  • What I don't understand is why `ArrayLike` (because I think this is the case here) is unwrapped - or rather I understand that it might be by design, but is it written anywhere? I couldn't find it on https://rxjs.dev/. – Jędrzej Dudkiewicz Oct 11 '22 at 23:19
  • Ok, here https://reactivex.io/rxjs/class/es6/MiscJSDoc.js~ObservableInputDoc.html it is written explicitly. But how are rxjs.dev and reactivex.io connected if at all - that I don't know. – Jędrzej Dudkiewicz Oct 11 '22 at 23:23
  • The official docs is rxjs.dev. I don't know who maintains reactivex.io. – martin Oct 12 '22 at 07:48
  • Weird, https://en.wikipedia.org/wiki/ReactiveX says that reactivex.io is Microsoft's effort. What is most interesting is that documentation seems much richer - if I haven't limited my search to rxjs.dev, which is an official site as you said, I would have found answer to my question on my own. Never the less thanks for an answer. – Jędrzej Dudkiewicz Oct 12 '22 at 08:04
  • rxjs.dev is a part of the RxJS github repo. I think that reactivex.io is a general documentation about Rx principles shared among all implementations. I'm just looking at the examples with RxJS they have and it's very outdated (most are probably for RxJS 5 or older). But you're right, the documentation (including the official one) is sometimes very confusing. – martin Oct 12 '22 at 10:06
2

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?

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • As I mentioned this is not something that I wrote, this is something I "inherited" from different company. Why is it done this way - I don't know, but I've seen it mentioned as a solution quite a few times here on SO (see for example this: https://stackoverflow.com/questions/42482705/best-way-to-flatten-an-array-inside-an-rxjs-observable). Also this is not something I want or not, I wanted to know why it behaves like this because it is counter-intuitive. As a side note, how would you change that? `mergeMap(x => from(x))`? I want it to be more explicit, since it works fine either way. – Jędrzej Dudkiewicz Oct 11 '22 at 22:06
1

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.

Jędrzej Dudkiewicz
  • 1,053
  • 8
  • 21