13

A very common problem when using RxJs seems to be to want the result of one or more observables to then use them in subsequent ones.

e.g. in pseudo-code (This is not rx or valid js syntax deliberately)

var someResult = $observable-A; // wait to complete
var finalResult = $observable-B(someResult.aValueINeed);

This can be done in an ugly way where you could subscribe to both and call one inside the other.. however, this is very messy and doesn't afford you much flexibility.

e.g. (real syntax)

$observable-A.subscribe(resultA => { 
    $observable-B(resultA.aValueINeed)
        .subscribe(resultB => { 
            console.log('After everything completes: ', resultB); 
        }
}

This also means that nothing else can easily consume this stream as you're completing both of the observables.

My specific use case requires the following:

  1. An initial call to get $observable-A as it doesn't require anything else (it's a basic http GET call)
  2. An http GET call to another service that requires data from $observable-A, which returns $observable-B
  3. Using both observable results (A + B) to create an object for my service (In my case, an Angular service) to return a single list.

I also need to be able to subscribe to this function inside my service, this is why going with the subscribe method above, won't work for me.

James
  • 2,516
  • 2
  • 19
  • 31

1 Answers1

21

Snorre Danielsen nailed this problem and full credit for this solution goes to him. I'd recommend having a look at this.

The great thing about rxjs operators is their interoperability. You can mix and match almost everything to get your desired outcome.

In short. The code answer to this question is below, and i'll explain it further.

$observable-A.pipe(
    mergeMap(resultA => {
        return combineLatest(
            of(resultA),
            $observable-B(resultA)
        )
    }),
    map(([resultA, resultB]) => {
        // you can do anything with both results here
        // we can also subscribe to this any number of times because we are doing all the processing with
        // pipes and not completing the observable
    }
)

mergeMap (or flatMap which is an alias) takes an observable as an input and projects it (just like a regular map function). This means we can use its result as the input for $observable-B. Now this is great for when you actually want to only return the second observable result. e.g.

$observable-A.pipe(
    mergeMap(resultA => $observable-B(resultA)),
    map((resultB) => {
        // resultA doesn't exist here and we can only manipulate resultB
    }
)

Now, back to the main solution. combineLatest is the key here. It allows the mergeMap to essentially "wait" for the internal observable ($observable-B) to complete. Without it, you're just going to return an observable to your next rxjs operator in the pipe, which is not what we want (as you're expecting to deal with regular operator functions inside a pipe).

Because of this, we can take these new merged observables into the next part of the chain and use them together. We use the of() function to re-create resultA, as combineLatest only takes observables as inputs and resultA is a completed observable in this state.

A hint: Take a look at the map(([resultA, resultB]). The reason we use this notation to refer to our variables instead of the standard map(results). Is so we can directly reference them without using results[0] for resultA or results[1] for resultB. Its just a lot easier to read this way.


I hope this helps people out as I have yet to find a complete SO answer which covers this case. Edits are very welcome as its complicated and I'm sure its not a perfect answer.

James
  • 2,516
  • 2
  • 19
  • 31
  • 2
    If both the observables are from HTTP requests, `switchMap` and `forkJoin` would be a better fit compared to `mergeMap` and `combineLatest`. Merging results from multiple requests is uncommon. `combineLatest` and `zip` operators have very specific use cases where you need the values from the source observables as soon as one emits (provided that all sources have emitted at least once). – ruth Jun 01 '20 at 22:13
  • Interesting, Why would this be the case? – James Jun 04 '20 at 12:51
  • 1
    `mergeMap` is used to merge multiple notifications from the source observable. In HTTP it's rarely the case. See [here](https://blog.angular-university.io/rxjs-higher-order-mapping/) for info on higher order mapping operators. As for `combineLatest`, it is used to obtain the result from a stream of data that doesn't complete immediately. But HTTP observables emits once and complete immediately, which is the specific case `forkJoin` is for. It emits only after the observables complete. – ruth Jun 09 '20 at 17:35