When we subscribe to observables or other resources within a component, it's crucial to unsubscribe or complete those subscriptions when the component is no longer in use. If we fail to clean up these subscriptions during the destruction phase, they will continue to hold references to resources, preventing them from being garbage-collected. As a result, memory consumption grows over time, impacting the performance and responsiveness of our application.
Angular 16 has recently been released, offering a range of exciting new features. One noteworthy addition is the introduction of an injectable entity called DestroyRef. This entity can be injected into various Angular building blocks, including components and services.
By utilizing DestroyRef, you can register a callback function within these building blocks. This callback function will be executed just before the associated scope is destroyed. It provides a convenient way to perform cleanup operations or additional logic before the scope is removed.
The DestroyRef feature simplifies the handling of pre-destruction actions across different Angular components and services. It ensures that necessary cleanup tasks are executed reliably, resulting in more robust and efficient Angular applications. With this new capability, developers can have better control over the destruction process of Angular scopes and enhance the overall performance of their applications.
All right! You might be wondering, "Isn't the ngOnDestroy hook already available in Angular? Why do we need DestroyRef?"
While ngOnDestroy is available, DestroyRef allows us to create reusable logic for performing cleanup tasks when a scope is destroyed, without the need for inheritance. This simplifies the implementation process and reduces complexity.
While ngOnDestroy is available, DestroyRef allows us to create reusable logic for performing cleanup tasks when a scope is destroyed, without the need for inheritance. This simplifies the implementation process and reduces complexity.
ngOnDestroy Exmaple:
typescriptimport { Component, OnDestroy, OnInit } from '@angular/core';
import { of, Subscription } from 'rxjs';
@Component({
selector: 'app-thecoderevisited',
templateUrl: './thecoderevisited.component.html',
styleUrls: ['./thecoderevisited.component.css']
})
export class ThecoderevisitedComponent implements OnInit, OnDestroy {
subscriptions: Subscription[] = [];
ngOnInit(): void {
this.subscriptions.push(of([]).subscribe());
}
ngOnDestroy(): void {
// Cleanup logic
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
In the above code, we have an Angular component called ThecoderevisitedComponent that implements the OnInit and OnDestroy interfaces. Inside the component, there is an array called subscriptions of type Subscription[], which will store all the subscriptions we create.
In the ngOnInit lifecycle hook, we push a new subscription to the subscriptions array using the of([]).subscribe() method. This is just a placeholder observable (of([])) that doesn't emit any values. You can replace it with your actual observable.
In the ngOnDestroy lifecycle hook, we iterate over the subscriptions array and call unsubscribe() on each subscription. This ensures that all subscriptions are properly unsubscribed when the component is destroyed, preventing any memory leaks.
Limitation with ngDestory() :
It's unnecessary to emphasize that these activities had to be duplicated in every component where subscriptions are used. It appears as an additional task to perform and extra code to include and remember.
As developers, we continually strive to improve and simplify our lives. Some implementations I've come across introduce a base component solely to centralize subscription management in one place. Personally, I'm not particularly fond of this approach as it introduces an additional layer of abstraction and inheritance. This, in turn, adds complexity to unit testing and requires additional considerations for super constructor calls in each component.
As a developer, finding a balance between code simplicity and maintainability is crucial. While consolidating subscription logic in a base component can provide a centralized approach, it's important to weigh the trade-offs and consider alternative solutions that minimize complexity and improve testability. An alternative solution is DestroyRef.
It's unnecessary to emphasize that these activities had to be duplicated in every component where subscriptions are used. It appears as an additional task to perform and extra code to include and remember.
As developers, we continually strive to improve and simplify our lives. Some implementations I've come across introduce a base component solely to centralize subscription management in one place. Personally, I'm not particularly fond of this approach as it introduces an additional layer of abstraction and inheritance. This, in turn, adds complexity to unit testing and requires additional considerations for super constructor calls in each component.
As a developer, finding a balance between code simplicity and maintainability is crucial. While consolidating subscription logic in a base component can provide a centralized approach, it's important to weigh the trade-offs and consider alternative solutions that minimize complexity and improve testability. An alternative solution is DestroyRef.
DestroyRef :
Using DestroyRef is a straightforward process. From angular 16 onwards we can inject the DestroyRef provider as follows and register a destroy callback in the following manner:
typescript@Component({
selector: 'foo',
standalone: true,
template: '',
})
class ThecoderevisitedComponent {
constructor() {
inject(DestroyRef).onDestroy(() => {
// Perform necessary cleanup tasks when the component is destroyed
});
}
}
As an example, we can create an untilDestroyed operator that relies on DestroyRef:
typescriptexport function untilDestroyed() {
const subject = new Subject();
inject(DestroyRef).onDestroy(() => {
subject.next(true);
subject.complete();
});
return <T>() => takeUntil<T>(subject.asObservable());
}
@Directive({
selector: '[appFoo]',
standalone: true,
})
export class ThecoderevisitedDirective {
private untilDestroyed = untilDestroyed();
ngOnInit() {
interval(1000)
.pipe(this.untilDestroyed())
.subscribe(console.log);
}
}
In this example, the untilDestroyed operator creates an observable that emits values until the associated scope is destroyed. It relies on the DestroyRef functionality to handle the destruction event. The ThecoderevisitedDirective uses this untilDestroyed operator within its ngOnInit lifecycle hook to subscribe to an interval observable and log the emitted values.
By leveraging DestroyRef and related utilities, we can simplify the process of performing cleanup tasks and ensure that our Angular components and directives are properly managed when destroyed.
Read more,