Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

The Proper Way to Destroy Angular Components

4.30/5 (9 votes)
29 Jul 2020CPOL4 min read 20.2K  
Best practices and techniques to properly destroy Angular components and unsubscribe from observables
In this article, we look at the two approaches for designing a component and how (if required) we should be implementing ngOnDestroy.

Every Angular App has multiple components which we use to represent different types of data. And, usually the data to display in a component comes in through an Observable wired to a Rest API call to the backend.

When we create a component in Angular, there are multiple ways in which we consume such an observable and hence we need to be mindful of how we dispose of these observables when the component goes out of scope/view.

Angular provides the ngOnDestroy lifecycle hook (in addition to the async pipe) which you can use to accomplish exactly these types of tasks.

Let’s look at the 2 approaches for designing a component and how (if required) should we be implementing ngOnDestroy.

(All the examples/code snippets below consume an observable which spits out numbers 0 to 40 at regular intervals.)

When using the Async Pipe

The Async pipe provides a cool abstraction and hides a lot of overhead when it comes to consuming and disposing an observable stream.

JavaScript
@Component({
  selector: 'app-async-pipe-sample',
  template: `
  <p>async-pipe-sample works!</p>
  <div>
      Latest value : {{ spitNumbersObservable | async }}
  </div>`,
  styleUrls: ['./async-pipe-sample.component.scss']
})
export class AsyncPipeSampleComponent implements OnInit {

  numbers = interval(1000);
  spitNumbersObservable: Observable<number>;
  constructor() {
    this.spitNumbersObservable = this.numbers.pipe(take(40));
  }

  ngOnInit(): void { }
}

Since we are using the Async Pipe in this example, we don’t have to bother about unsubscribing from the observables. Since the observable is bound to an element on the page, it gets unsubscribed as soon as the page is destroyed.

When Not Using the Async Pipe

Async Pipes are convenient but when you are designing a complex Angular view, it’s quite possible that you aren’t binding to a single observable. Also, you might be subscribing to multiple (sometimes dependent) observables to populate a view.

In these cases, using the Async pipe might not be straightforward as you may want more control on the binding.

Let’s look at an example of a Component with a Leak:

JavaScript
@Component({
  selector: 'app-ng-destroy-sample',
  template: `<p>Ng Destroy Leaky</p>`,
  styleUrls: ['./ng-destroy-sample.component.scss']
})
export class NgDestroySampleComponent implements OnInit {

  numbers = interval(1000);
  spitNumbersObservable: Observable<number>;
  constructor() {
    this.spitNumbersObservable = this.numbers.pipe(take(40));
  }

  ngOnInit(): void {
    this.spitNumbersObservable.subscribe(
      n => console.log(n)
    );
  }
}

// This prints console logs
// LEAKY Latest value : 0
// LEAKY Latest value : 1
// LEAKY Latest value : 2
// LEAKY Latest value : 3
// LEAKY Latest value : 4
// ..
// ..

The code looks fine, but since we are not unsubscribing the observable, even if we navigate away from the component, the observable is still subscribed (and the console logs still print).

As you might have guessed by now, the proper way to destroy a component would be to destroy all observable listeners/subscribers as well.

How to Dispose Your Observables

Using unsubscribe()

I would say this is the documented way of unsubscribing an observable. You subscribe to it, so at some point you should unsubscribe.

JavaScript
export class NgDestroyUnsubSampleComponent implements OnInit, OnDestroy {
  numbers = interval(1000);
  spitNumbersObservable: Observable<number>;
  private subscription1: Subscription;
  constructor() {
    this.spitNumbersObservable = this.numbers.pipe(take(40));
  }
  ngOnDestroy(): void {
    this.subscription1.unsubscribe();
  }

  ngOnInit(): void {
    this.subscription1 = this.spitNumbersObservable.subscribe(
      n => console.log(`Latest value : ${n}`)
    );
  }
}

The approach works well, but I wouldn’t say it’s my favorite approach.

When there are multiple observables subscribed in a component, the ngOnDestroy method starts getting very crowded as you have to keep track of all subscriptions.

JavaScript
ngOnDestroy(): void {
   this.subscription1.unsubscribe();
   this.subscription2.unsubscribe();
   this.subscription3.unsubscribe();
   this.subscription4.unsubscribe();
 }

Using takeUntil(..)

RxJs has an operator called takeUntil which basically just monitors another (boolean) observable to decide if it has to take any more items from the observable stream. The moment the boolean observable emits ‘true’, it stops accepting any more values from the observable and actually completes the observable (preventing any more values from being emitted/consumed).

JavaScript
export class NgDestroyTakeuntillSampleComponent implements OnInit, OnDestroy {
  numbers = interval(1000);
  spitNumbersObservable1: Observable<number>;
  spitNumbersObservable2: Observable<number>;
  spitNumbersObservable3: Observable<number>;
  _destroyed$ = new Subject<boolean>();
  constructor() {
    this.spitNumbersObservable1 = this.numbers.pipe(take(40));
    this.spitNumbersObservable2 = this.numbers.pipe(take(40));
    this.spitNumbersObservable3 = this.numbers.pipe(take(40));
  }
  ngOnDestroy(): void {
    this._destroyed$.next(true);
  }

  ngOnInit(): void {
    this.spitNumbersObservable1
      .pipe(takeUntil(this._destroyed$)).subscribe(
        n => console.log(`Latest value : ${n}`)
      );
    this.spitNumbersObservable2
      .pipe(takeUntil(this._destroyed$)).subscribe(
        n => console.log(`Latest value : ${n}`)
      );
    this.spitNumbersObservable3
      .pipe(takeUntil(this._destroyed$)).subscribe(
        n => console.log(`Latest value : ${n}`)
      );
  }
}

As you can see, we end up with a very clean and concise ngOnDestroy method which tells the programmer that as long as you use the _destroyed$ observable to control fetching items from an observable, everything will be disposed as soon as ngOnDestroy is called.

More Optimized Usage – Using take(1)

A lot of times when we fetch data into our Angular component, say from a REST API, we don’t expect that value to change very often.

For example, say I’m fetching a value of a feature flag from a REST API call, I know I can just take the first value from the observable and be done with it. I just require that value to initialize the behavior of the component (the first time).

For these scenarios, we can go further and use the operator take() from rxjs and just get the first value emitted from the observable. After the first value, the observable actually completes (preventing any more values from being emitted/consumed) and there is nothing the developer has to do to manage it.

JavaScript
export class TakeOneSampleComponent implements OnInit {
  numbers = interval(1000);
  spitNumbersObservable: Observable<number>;
  constructor() {
    this.spitNumbersObservable = this.numbers.pipe(take(40));
  }

  ngOnInit(): void {
    this.spitNumbersObservable.pipe(take(1)).subscribe(
      n => console.log(`Latest value : ${n}`)
    );
  }
}

// Prints the below value only
// Latest value : 0

Well, if you start thinking about it, there are actually a lot of places where you can get away with using take(1). Not all observable data in your view requires it to change every time a new item is pushed into the observable. In fact, some observables (backed by REST API calls) do not even emit more than 1 value.

To summarize all this, make sure you understand the observables you are using in your code and implement the right way to unsubscribe from all of them as soon as the component is destroyed.

All the code samples expressed in this article are available at this GitHub repo.

Happy coding!

History

  • 27th July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)