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.
@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:
@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)
);
}
}
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.
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.
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).
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.
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}`)
);
}
}
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