DestroyRef has been introduced in Angular 16 (commit link). It gives us the option to run a callback function when the component/directive is destroyed or when the corresponding injector is destroyed.
Let’s see an easy example to understand how we can use that.
Callback when a component is being destroyed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { Component } from '@angular/core'; import { interval } from 'rxjs'; @Component({ selector: 'app-dashboard', standalone: true, template: ``, }) export default class DashboardComponent { constructor() { interval(1000).subscribe((value) => { console.log(value); }); } } |
The code above emits a new value every 1 sec (1000ms) and logs a value to the console. It’s a small piece of code, but it still creates a memory leak since we are not destroying the subscription.
Let’s answer some questions you may have.
Q: What would happen if we changed the route?
A: Well, the component would be destroyed.
Q: What would happen if we came back to this route?
A: Well, the component would be constructed again.
Despite the component being destroyed, the subscription remains active.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Component, OnDestroy } from '@angular/core'; import { Subscription, interval } from 'rxjs'; @Component({ selector: 'app-dashboard', standalone: true, template: ``, }) export default class DashboardComponent implements OnDestroy { #subscription?: Subscription; constructor() { this.#subscription = interval(1000).subscribe((value) => { console.log(value); }); } ngOnDestroy(): void { this.#subscription?.unsubscribe(); } } |
We have to unsubscribe the subscription to avoid creating a memory leak. But perhaps you are already doing this 👍
Let’s do the same, but this time using DestroyRef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Component, DestroyRef, inject } from '@angular/core'; import { Subscription, interval } from 'rxjs'; @Component({ selector: 'app-dashboard', standalone: true, template: ``, }) export default class DashboardComponent { #subscription?: Subscription; #destroyRef = inject(DestroyRef); constructor() { this.#subscription = interval(1000).subscribe((value) => { console.log(value); }); this.#destroyRef.onDestroy(() => { this.#subscription?.unsubscribe(); }); } } |
Let’s read the code from top to bottom.
- We are creating a #destroyRef instance using the inject method. Please note that this is happening during the injection context.
- We are registering a callback function in the onDestroy method. The given function will be executed when the component is being destroyed.
Alternatively, we could write the same piece of code like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export default class DashboardComponent { #subscription?: Subscription; constructor() { this.#subscription = interval(1000).subscribe((value) => { console.log(value); }); inject(DestroyRef).onDestroy(() => { this.#subscription?.unsubscribe(); }); } } |
Note: This time, we are using the inject function in the constructor. This still works fine since we are in the injection context.
There is a better way to unsubscribe, though. Keep reading 🙂
TakeUntilDestroyed
Before we look at a better way to unsubscribe, let’s dig into some important details.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
export default class DashboardComponent { #subscription?: Subscription; myTakeUntilDestroyed() { inject(DestroyRef).onDestroy(() => { this.#subscription?.unsubscribe(); }); } constructor() { this.#subscription = interval(1000).subscribe((value) => { console.log(value); }); this.myTakeUntilDestroyed(); } } |
I have created the method myTakeUntilDestroyed, which injects
DestroyRef.
It’s important to understand that we cannot use the inject method outside the injection context.
In the example above, I call myTakeUntilDestroyed from the constructor, which works fine.
Injection Context: Constructor, class fields, factory method. Read more
What would happen if we call the method from the ngOnInit hook?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
export default class DashboardComponent implements OnInit { #subscription?: Subscription; myTakeUntilDestroyed() { inject(DestroyRef).onDestroy(() => { this.#subscription?.unsubscribe(); }); } constructor() { this.#subscription = interval(1000).subscribe((value) => { console.log(value); }); } ngOnInit(): void { this.myTakeUntilDestroyed(); } } |
Since we are not in the injection context, Angular will throw an error.
If we, however, have to call myTakeUntilDestroyed from the
ngOnInit hook, we should change how we access
DestroyRef.
1 2 3 4 5 |
myTakeUntilDestroyed(destroyRef?: DestroyRef) { (destroyRef ?? inject(DestroyRef)).onDestroy(() => { this.#subscription?.unsubscribe(); }); } |
This change allows the developer to use myTakeUntilDestroyed outside of the injection context. As such, the code will become:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export default class DashboardComponent implements OnInit { #subscription?: Subscription; #destroyRef = inject(DestroyRef); myTakeUntilDestroyed(destroyRef?: DestroyRef) { (destroyRef ?? inject(DestroyRef)).onDestroy(() => { this.#subscription?.unsubscribe(); }); } constructor() { this.#subscription = interval(1000).subscribe((value) => { console.log(value); }); } ngOnInit(): void { this.myTakeUntilDestroyed(this.#destroyRef); } } |
So far, we have covered some important details, and we are now ready to start using the takeUntilDestroyed rxjs operator.
takeUntilDestroyed
completes the observable when the component/directive is destroyed or when the corresponding injector is destroyed!
1 2 3 4 5 6 7 8 9 10 11 |
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export default class DashboardComponent { constructor() { interval(1000) .pipe(takeUntilDestroyed()) .subscribe((value) => { console.log(value); }); } } |
That’s great! We have achieved the same with less and easy-to-read code. Nice!
Oh, wait, how about the ngOnInit hook?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export default class DashboardComponent implements OnInit { #destroyRef = inject(DestroyRef); ngOnInit(): void { interval(1000) .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe((value) => { console.log(value); }); } } |
If we have to use the takeUntilDestroyed operator outside the injection context, we (the developers) are responsible for providing
DestroyRef, similar as in our custom myTakeUntilDestroyed method.
If you enjoy watching videos, you must take a look at this one that covers the same content as the article
Get To Know the Angular DestroyRef
Useful links:
Thanks for reading my article!
Leave a Reply