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 </span><b>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 </span><span style="font-weight: 400;">myTakeUntilDestroyed
, which injects </span><span style="font-weight: 400;">DestroyRef
.
It’s important to understand that we cannot use the inject method outside the injection context.
In the example above, I call </span><span style="font-weight: 400;">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 </span><span style="font-weight: 400;">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 </span><span style="font-weight: 400;">myTakeUntilDestroyed
from the </span><span style="font-weight: 400;">ngOnInit
hook, we should change how we access </span><span style="font-weight: 400;">DestroyRef
.
1 2 3 4 5 |
myTakeUntilDestroyed(destroyRef?: DestroyRef) { (destroyRef ?? inject(DestroyRef)).onDestroy(() => { this.#subscription?.unsubscribe(); }); } |
This change allows the developer to use </span><span style="font-weight: 400;">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 </span><span style="font-weight: 400;">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 </span><span style="font-weight: 400;">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 </span><span style="font-weight: 400;">takeUntilDestroyed
operator outside the injection context, we (the developers) are responsible for providing </span><span style="font-weight: 400;">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