36

There are several ways to unsubscribe from observables on Angular components (by using ngOnDestroy). Which option below should be preferred and why (e.g. technical reasons, performance, etc.)?

Option 1: takeUntil

Using RxJS takeUntil to unsubscribe

@Component({
  selector: "app-flights",
  templateUrl: "./flights.component.html"
})
export class FlightsComponent implements OnDestroy, OnInit {
  private readonly destroy$ = new Subject();

  public flights: FlightModel[];

  constructor(private readonly flightService: FlightService) {}

  ngOnInit() {
    this.flightService
      .getAll()
      .pipe(takeUntil(this.destroy$))
      .subscribe(flights => (this.flights = flights));
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Option 2: .unsubscribe()

Explict call .unsubscribe(), e.g. by using a separate subscription instance

@Component({
  selector: "app-flights",
  templateUrl: "./flights.component.html"
})
export class FlightsComponent implements OnDestroy, OnInit {
  private readonly subscriptions = new Subscription();

  public flights: FlightModel[];

  constructor(private readonly flightService: FlightService) {}

  ngOnInit() {
    const subscription = this.flightService
      .getAll()
      .subscribe(flights => (this.flights = flights));

    this.subscriptions.add(subscription);
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}

Option 3: takeWhile

Using RxJS takeWhile unsubscribe

Option n: ?

Horace P. Greeley
  • 882
  • 1
  • 10
  • 18
  • 7
    You could ask ten people and they all could have their own preference. All the options you mentioned are perfectly valid and there's no clear 'best'. IMO takeUntil is the best solution. – Giovani Vercauteren Oct 17 '19 at 09:05
  • ok, I was wondering if there are technical reasons for one or another options... Thanks for your comment – Horace P. Greeley Oct 17 '19 at 09:09
  • @HoraceP.Greeley I know it's late but why the need for this.destroy$.next(); on Option 1? – raberana Feb 24 '21 at 14:09
  • @raberana next() is required due to "takeUntil" passes until destroy$ has a value by calling destory$.next(). Another topic, but important: takeUntil should be at the last position in the pipe. See https://cartant.medium.com/rxjs-avoiding-takeuntil-leaks-fb5182d047ef – Horace P. Greeley Feb 26 '21 at 13:56

3 Answers3

19
  • Option 1: clean & explicit. Works like a charm
  • Option 2: more procedural, less stream-like. Works like a charm. Note that your stream will not get a 'complete' event which can cause unexpected behaviour
  • Option 3: takeWhile - will have the subscription stay around untill an emission is created and then the takeWhile is evaluated. This can lead to unexpected behaviour. Still, in the end works

TLDR; there is no wrong here. Choose what you see fits your needs and communicates your intent.

Ben Lesh has also written quite a good post about the different ways to unsubscribe https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87

His opinion:

You should probably be using operators like takeUntil to manage your RxJS subscriptions. As a rule of thumb, if you see two or more subscriptions being managed in a single component, you should wonder if you could be composing those better.

Mark van Straten
  • 9,287
  • 3
  • 38
  • 57
  • Should we use `this.destroy$.next()` and `this.destroy$.complete()` in `ngOnDestroy()` when using `takeUntil`? – Jack Dec 26 '20 at 15:29
  • `takeUntil` needs an emission so you have to `next()` something (even `undefined)`. `complete()` is redundant given that nobody is listening to that event. – Mark van Straten Jan 04 '21 at 15:26
  • Does it mean that we do not need this.destroy$.next() and this.destroy$.complete() in ngOnDestroy() when using takeUntil? – Jack Jan 05 '21 at 07:44
14

Personally, I choose option 1 as well, which is to use takeUntil.

However, it is important for you to place the takeUntil() operator on the last RxJS operator on the pipe sequence, as explained here.

For instance, if we do not place takeUntil() as the last operator, the subscriber will remain subscribed to the subsequent switchMap() operator:

this.flightService.getAll()
  .pipe(
    takeUntil(this.destroy$),
    switchMap(() => this.doSomethingElse()),
  ).subscribe(flights => (this.flights = flights));

Therefore, the right way of doing so would be this:

this.flightService.getAll()
  .pipe(
    switchMap(() => this.doSomethingElse()),
    takeUntil(this.destroy$),
  ).subscribe(flights => (this.flights = flights));

Brian Love has written a really good article on the different ways (including unsubscribe(), and the takeUntil() operator) of unsubscribing to observables. I would recommend you to take a look at it as well.

w5l
  • 5,341
  • 1
  • 25
  • 43
wentjun
  • 40,384
  • 10
  • 95
  • 107
  • Should we use `this.destroy$.next()` and `this.destroy$.complete()` in `ngOnDestroy()` when using `takeUntil`? – Jack Dec 26 '20 at 15:29
  • @Fredrick yes we should! Apologies, my answer did not reflect that. – wentjun Jan 01 '21 at 14:38
  • Then I think using `unsubscribe` seems to be better instead of `takeUntil`. Because it is shown as a showter version, but almost similar that we also call a method `onDestroy`. Is not it? – Jack Jan 01 '21 at 15:55
  • @Fredrick yes, but if you have multiple subscriptions in your component, it will be cleaner to use the `takeUntil` approach. I believe this article gives a full explanation https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 – wentjun Jan 01 '21 at 17:19
  • Thanks, but I also read [this](https://www.intertech.com/angular-best-practice-unsubscribing-rxjs-observables/) article and see that there is almost no difference between two approaches regarding to code lengths (one of them is inner, the other is outer block). – Jack Jan 01 '21 at 17:31
  • takeUntil is more flexible. the position of takeUntil in operators is important. but most of time we need that takeUntil is the last operator and in this time is equal to unsubscribe. – Hamid Taebi Jul 20 '21 at 13:20
2

For your example I wouldn't choose any of the 3 options. When you only want to assign a value, then store the Observable and subscribe to it with async pipe.

@Component({
  selector: "app-flights",
  templateUrl: "./flights.component.html"
})
export class FlightsComponent implements OnDestroy, OnInit {  
  public flights$: Observable<FlightModel[]>;

  constructor(private readonly flightService: FlightService) {}

  ngOnInit() {
    this.flights$ = this.flightService.getAll();
  }
}

<ng-container *ngIf="flights$ | async as flights">
  <ng-container *ngFor="let flight of flights">
    {{ flight | json }}
  </ng-container>
</ng-container>
MoxxiManagarm
  • 8,735
  • 3
  • 14
  • 43
  • 1
    His examples are contrived to provide context for the question, while your answer is the right thing to do in a real situation, it doesn't really help with the specific question. – claudekennilol Mar 10 '21 at 17:18