Returning An Observable From A Subscription

I’ve seen some diabolical ways Angular developers have tried to “subscribe to an observable.. but return that same observable”, or at least that’s how it was explained to me when I saw the following code :

getData() { 
  let observable = this.myService.getData();

  observable.subscribe(x => 
  {
    //do something with the data here
  });

  //Also return the observable here. 
  return observable;
}

This is very bad practice for a very simple reason. Each subscription to an observable, for the most part, will execute the observable method. Remember, observables are typically lazy loading and therefore execute only on a subscription. But they also execute *every* subscription.

In the above code, should someone subscribe to the observable returned from the method, it will also execute the underlying observable. If this is an API call for instance, you will end up calling the endpoint twice.

So what’s the right way to do this? It actually all depends on how getData() is being called, and what you need the data for *inside* the method.

If You Need Access To The Data You Will Then Return Anyway, Use Tap

We actually have a full article on how the Tap operator works right here. But if we take our above example, it would work a little like this :

getData() {
  return this.myService.getData().pipe(tap(x => 
  {
    //Do something with the data here. 
  }));
}

This works if you need access to the result (For example for caching), but you don’t intend to edit the result (For example map it to another type). And the caller to getData() should just get whatever the result from the service was.

If You Need To Modify The Result Before Returning, Use Map

This one is again pretty straight forward :

getData() {
  return this.myService.getData().pipe(map(x => 
  {
    return new MappedClass(x.value);
  }));
}

We use Map if we want to modify the result before returning it to the caller. It should be noted that again, if you don’t want to manipulate the result at all, then use Tap.

If The Caller Doesn’t Need The Result At All, And May Or May Not Subscribe, Use Replay Subject

This one might be a bit complicated, but here it is :

getData() {
  let subject = new ReplaySubject(1);

  this.myService.getData().subscribe(x => 
  {
    //Do something here. 
    subject.next();
    subject.complete();
  });

  return subject;
}

We create a ReplaySubject that has a buffer size of 1. (For why we use ReplaySubject here and not Subject, see here). Then then call the service, and on the completed subscription, complete the ReplaySubject, letting any callers know (If they care at all!) that we are done.

This has a couple of benefits :

  1. The call to the service is made regardless of whether this method is subscribed to or not. For example if you are calling this on button press (Where you don’t care about the result) versus calling it somewhere else in your component (Where you do care about the result), it doesn’t matter.
  2. The ReplaySubject is import as the call to myService might complete before a subscription is added to the returned subject.

I do want to point out that on the original subscription, you may need to unsubscribe/complete the observable returned from myService, but the point stands that a ReplaySubject can be a great way to let a caller subscribe “if they want”, but if they don’t things still run fine.

If The Caller Is Marked Async, Or You Prefer Promises, Then Use Promises

This is certainly not ideal. In most cases, you should try and stick with using Observables in an Angular application. But if you really need to, you can switch to promises *if you are sure there will be no negative side effects* . The most important being that you understand that how promises and observables can be very similar, but do have some distinct differences.

The code would look like so :

async getData() {
  let data = await this.myService.getData().toPromise();

  //do something with the data. 

  //return the data if we want. 
}

Leave a Reply

Your email address will not be published. Required fields are marked *