Ben Lesh has a terrific article called “Don’t Unsubscribe”, telling you why you are better off not explicitly unsubscribing, but instead relying on constructs that cause the unsubscriptions to happen. In that spirit, I wanted to title this article “Don’t Even Subscribe”, but the truth is, there are a few places where you have to subscribe, so I settled for the less-emphatic title you see.

When should you subscribe? The answer to that question is, “Only when you absolutely have to.” Because (among other reasons) if you don’t subscribe, you don’t have to unsubscribe.

So, when do you absolutely have to subscribe?

In Services

So far as I can tell, you never have to subscribe to Observables inside services.

In general, services exist to provide data to other entities. A component or a directive needs some data, it asks a service, and that service returns an Observable that will eventually supply that data. There is no reason for the service itself to subscribe.

Of course, a service might call another service, and get back an Observable. You (as the author of the first service) might consider subscribing to the Observable, but remember: your service only made that call because it was itself called. Somewhere at the bottom of the calling chain, there must be a UI element, like a component or directive — and if the service returns the Observable, the component or directive can do the subscription.

The only case where the service might subscribe is if a method is called with an Observable and the service method’s job is to do something with the data that the Observable eventually emits. Even in that case, it seems likely that the best practice would be, instead of subscribing explicitly, create from that Observable a new Observable meaning “I’m done” and requiring the calling function to subscribe to that.

Say a service is given an Observable and the job is, collect all the data that Observable ever emits and write it to another service. Yes, that could be done by subscribing the Observable, and upon completion, writing the data. But instead, it could be written as:

class AccumLogService {
  constructor(private simpleLogService: SimpleLogService) {}
  logAllThisData(data: Observable<string>): Observable<void> {
    return data.pipe(reduce((acc: string[], v: string[]) => acc.concat([v]), []),
                     concatMap(total => this.simpleLogService.logArray(total)));
  }
}

Now the calling function is (properly) responsible for subscription.

Programmers have a tendency to want to write services that the user can “fire and forget”, by returning an Observable that is already hot, already working, but experience has shown this is not ideal. In some cases, “fire and forget” is not reasonable, and then you have some methods returning hot Observables and other ones returning cold ones, leading to confusion. Better to bite the bullet and just insist that subscribing is always the calling function’s responsibility.

Is there some case where either returning an Observable is not feasible, or the service wants to retain the subscription, perhaps for cancellation? I don’t know for sure, but I have not seen such a case, and I have not been able to contrive one.

The answer to the question “When To Subscribe In Services?” seems to be, “Only when you absolutely have to — and you almost never ‘absolutely’ have to.

In Components

In the real world, a lot of components are misusing subscription. There’s a lot of code out there that unfortunately looks like this:

/**
 * terrible use of .subscribe()
 * Do NOT do this
 */
@Component({
  template: `The current value is {{currentValue}}`,
})
export class SomeComponent {
  currentValue: string;
  constructor(someService: SomeService) {
    someService.getSomeObservable().subscribe(v => {
      this.currentValue = v;
    });
  }
}

What you should notice first is that the code leaks. The subscription is never unsubscribed, so if the Observable does not complete on its own, that whole component, and its template and all their associated objects, will live in memory forever.

You could fix it up, like so:

/**
 * very bad use of .subscribe()
 * Do NOT do this either
 */
@Component({
  template: `The current value is {{currentValue}}`,
})
export class SomeComponent implements OnDestroy {
  private readonly onDestroy = new Subject<void>();
  currentValue: string;
  constructor(someService: SomeService) {
    someService.getSomeObservable()
      .pipe(takeUntil(this.onDestroy))
      .subscribe(v => {
      this.currentValue = v;
    });
  }
  ngOnDestroy() {
    this.onDestroy.next();
  }
}

(See here for more discussion of the takeUntil() idiom.)

This version is much better, it does not leak outright, but it is still somewhat over-complicated. It is carefully copying data into the component, which does not care. The component doesn’t do anything with the data, it’s just holding it for the template. Moreover, now the component’s state has changed, but the template still has to be updated. If you are using the CheckAlways change-detection strategy, that change will be noticed automatically, but CheckAlways is slow and expensive; if you are using the (otherwise preferable) OnPush change-detection strategy, you have to remember to manually call markForCheck() on the ChangeDetectorRef.

Generally, the pattern of “subscribe and in the subscription function, copy data into the state of the component” is not a healthy one. It gets you unnecessarily involved in change detection and, relevant to this article, it’s an unnecessary subscription.

Better (more performant, clearer, more idiomatic) to let Angular handle the Observable directly, using the async pipe:

@Component({
  template: `The current value is {{currentValue | async }}`,
})
export class SomeComponent  {
  constructor(private someService: SomeService) {}
  
  currentValue = this.someService.getSomeObservable()
                     .pipe(share());
}

(See here for more discussion of replacing subscriptions with async.)

So, is the answer to the question “When To Subscribe In Components?” also “Only when you absolutely have to — and you almost never ‘absolutely’ have to”?

Sadly, no. There are at least two cases where you should explicitly subscribe to Observables in components and directives.

First is when the Observable makes a change to the outside world. Most commonly, this is done by issuing a POST (or DELETE or PUT) through HTTP, meant to update the backend.

Second is when the component itself — and not Angular via a template — is consuming the data. You see this when a component opens a modal dialog or send a message to the user, like a popup or a “snackbar”.

In both cases, the component is the thing that “wants” the Observable to actually execute, so it should be the one to subscribe.

Luckily, those two cases usually happen together: the user asks for some permanent change, and the code makes it and then tells the user that it was made. Here is a very simple case:

@Component({
  template: `<button (click)="buttonPress.next()">Press Me</button>`,
})
export class SomeComponent implements OnDestroy {
  private readonly onDestroy = new Subject<void>();
  readonly buttonPress = new Subject<void>();
  
  constructor(someService: SomeService,
              snackBar: MatSnackBar) {
    this.buttonPress.pipe(
      concatMap(() => someService.logButtonPress()),
      takeUntil(this.onDestroy))
    .subscribe(v => {
        this.snackBar.open("Button Press Logged!");
    });
  }
  ngOnDestroy() {
    this.onDestroy.next();
  }
}

There is a little bit of controversy here. This code changes “the outside world” in two places — the logButtonPress(), where it writes to the log, and the snackbar, where it tells the user what has happened — which is why it needs a subscription at all, but the first change doesn’t occur in the subscription, it occurs in the concatMap(), conflicting with the general rule that functions inside operators should be “pure” and not change the outside world like that.

The alternative, however, is to have nested subscriptions: subscribe to the button press and in the subscription function, invoke logButtonPress() and subscribe to its returned Observable, invoking the snackbar in that inner subscription.

The collective wisdom is that the first method is less-bad, at least in part because there is no reason that the nesting would stop at two levels: maybe you have several asynchronous, mutating changes that have be made, and several UI updates that have to be made when they complete.

However you do it, though, you will have to subscribe at least once.

Unfortunately for my catchy title, subscribing is more widely necessary than unsubscribing, but still, only do it when it is, in fact, necessary.

Thanks to Alex Okrushko and Max Koretskyi.