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
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.
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`
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:
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.
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?
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`.
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:
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!
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?
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!