80

I need to pass three data to one function from three different APIs:

this.service.service1().subscribe( res1 => {
  this.service.service1().subscribe( res2 => {
    this.service.service1().subscribe( res3 => {
      this.funcA(res1, res2, res3);
  });
  });
});

Is it a good practice to subscribe inside a subscribe?

Liam
  • 27,717
  • 28
  • 128
  • 190
Yashwanth Gurrapu
  • 1,039
  • 1
  • 9
  • 14

7 Answers7

99

The correct way is to compose the various observables in some manner then subscribe to the overall flow — how you compose them will depend on your exact requirements.

If you can do them all in parallel:

forkJoin(
  this.service.service1(), this.service.service2(), this.service.service3()
).subscribe((res) => {
  this.funcA(res[0], res[1], res[2]);
});

If each depends on the result of the previous, you can use mergeMap (formerly known as flatMap) or switchMap:

this.service.service1().pipe(
  mergeMap((res1) => this.service.service2(res1)),
  mergeMap((res2) => this.service.service3(res2))
).subscribe((res3) => {
  // Do something with res3.
});

... and so on. There are many operators to compose observables to cover lots of different scenarios.

Mark Hughes
  • 7,264
  • 1
  • 33
  • 37
  • 1
    does `forkJoin` ensure the sequence of execution? – Pankaj Parkar Sep 13 '18 at 16:30
  • 3
    It will return the results in the order they are coded, but I don't know if it will guarantee to start them in the order specified. However, as they are HTTP requests, even if they are started in a particular order, it is impossible to guarantee what order they will be received and processed by the server unless you form it sequentially - so if that is important, you need to use something like the flatMap chain where you wait for one before starting the next @PankajParkar – Mark Hughes Sep 13 '18 at 16:48
  • 2
    Assuming these are HTTP requests, how would you handle HTTP errors in each case, especially in the flatMap example? Also, if I'm reading the following right, if we need to ensure the order of completeness, we need to use `concatMap` instead of `flatMap`: https://www.learnrxjs.io/operators/transformation/concatmap.html – AsGoodAsItGets Jan 23 '19 at 13:47
  • 4
    concatMap vs flatMap would depend on your use case @AsGoodAsItGets - either would ensure order of completion in the example above. For error handling, in the above flatMap example, if (say) service2 errors, then service3 would not be called and it would go to the error handling function passed to the subscribe (if you passed one). If you want to catch them earlier, you could use catchError in the pipe... – Mark Hughes Jan 31 '19 at 16:37
  • 2
    What if you would like to handle errors from each service individually? I.e. one error handler for service 1 failure, another error handler for service 2, and so on? I can't imagine a practical situation with more than 2 subscriptions anyway, but just wondering how it would be structured. – AsGoodAsItGets Feb 01 '19 at 10:38
  • 1
    flatMap @deprecated — renamed. Use {@link mergeMap} – Vasyl Petrov Jan 04 '21 at 11:03
  • updated to use mergeMap instead, but added a note as there are many comments here referencing flatMap which wouldn't make sense @VasylPetrov – Mark Hughes Jan 05 '21 at 11:55
14

Though all of the above help provide solutions to this particular problem none of them seem to address the obvious underlying problem here, specifically:

Is it good way to call subscribe inside subscribe?

tl;dr

No it is not good to call a subscribe inside a subscribe.


Why?

Well because this is not how functional programming is supposed to work. You're not thinking functionally you're thinking procedurally. This isn't necessarily a problem per se, but the whole point of using rxjs (and other reactive programming extensions) is to write functional code.

I'm not going into all the details on what functional programming is but essentially the point of functional programming is to treat data as streams. Streams that are manipulated by functions, and consumed by subscribers. As soon as you add a subscribe inside another subscribe your manipulating data inside a consumer (not inside a stream). So your functional stream is now broken. This prevents other consumers from utilising that stream further dow in your code. So you've turned your functional stream into a procedure.

enter image description here

Image source, above and more information on pure functional programming here.

Liam
  • 27,717
  • 28
  • 128
  • 190
6

You can use forkJoin to combine the Observables into a single value Observable

forkJoin(
  this.service.service1(),
  this.service.service2(),
  this.service.service3()
).pipe(
  map(([res1, res2, res3 ]) => {
    this.funcA(res1, res2, res3);
  })
Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
Antoine V
  • 6,998
  • 2
  • 11
  • 34
  • 2
    I don't think that it's equivalent, cause you call it here in parallel, while the author do it sequentially – Yura Oct 05 '21 at 21:32
2

If the calls can be resolved in parallel you could use forkJoin, like this:

joinedServiceCalls() {
   return forkJoin(this.service1(), this.service2(), this.service3());
}

And then subscribe to that method. https://www.learnrxjs.io/operators/combination/forkjoin.html

Diego Pedro
  • 500
  • 4
  • 9
1

Looks strange, I would go this way because it looks cleaner:

async myFunction () {
//...
const res1 = await this.service.service1().toPromise();
const res2 = await this.service.service2().toPromise();
const res3 = await this.service.service3().toPromise();
this.funcA(res1, res2, res3);
//...

}

EDIT

or to do it in parallel

async myFunction () {

//...
let res1;
let res2;
let res3;
[res1,res2,res3] = await Promise.all([this.service.service1().toPromise(),
                                      this.service.service2().toPromise(),
                                      this.service.service3().toPromise()]);
this.funcA(res1, res2, res3);
//...

}
hjbello
  • 643
  • 4
  • 18
  • 1
    This suffers from the same problem that you make the three calls in sequence rather than in parallel, which is unnecessarily slow (assuming they could run in parallel instead). – Ingo Bürk Sep 13 '18 at 16:02
  • 4
    The best practice answer for an rxjs approach shouldn't be "convert everything to promises" - it may work in certain scenarios (such as HTTP calls) but the beauty of Observables is they work on streams not single-shot responses like promises - converting them to promises loses the whole point of using observables. – Mark Hughes Sep 13 '18 at 16:12
0

You can use the zip RxJs operator, and then in this case you will only use just one subscribe.

You can then call your function inside that subscribe because all the results are available.

Observable.zip(
  this.service.service1(),
  this.service.service1(),
  this.service.service1()
).subscribe([res1, res2, res3]) {
  this.funcA(res1, res2, res3);
}
HDJEMAI
  • 9,436
  • 46
  • 67
  • 93
0

as mentioned, forkjoin is a good solution, but it emit completed calls only. If these are values that are going to be emitted repeatedly, use I would combineLatest.

user856510
  • 19
  • 1