A common issue with older Angular versions was to automatically unsubscribe to RxJs subscriptions at the end of life of e.g. a Component. This is essential to avoid memory leaks and to prevent the application from slowing down over time.
Subscription array
Over many years a typical pattern was to collect subscriptions in an array and to unsubscribe to them in the Angular-"OnDestroy"-Lifecycle-Hook "ngOnDestroy".
Typical pattern is described below:
@Component({
selector: 'app-array-example-component',
standalone: true,
imports: [],
templateUrl: './array-example-component.component.html',
styleUrl: './array-example-component.component.scss'
})
export class ArrayExampleComponentComponent implements OnInit, OnDestroy {
/**
* DI
*/
#activatedRoute = inject(ActivatedRoute);
/** array of subscriptions registered and to be unsubscribed */
#subscriptions: Subscription[] = [];
/**
* Angular lifecycle hook to initialize the component
*/
ngOnInit(): void {
this.#subscriptions.push(
this.#activatedRoute.paramMap.pipe(
map(params => params.get('username')),
).subscribe()
);
}
/**
* Angular lifecycle hook to destroy the component
*/
ngOnDestroy(): void {
this.#subscriptions.forEach(sub => sub.unsubscribe());
}
}
What happens is:
- In the "ngOnInit" method, the subscription is created and added to the array "#subscriptions".
- In the "ngOnDestroy" method, the array "#subscriptions" is iterated and each subscription is unsubscribed.
Fine and save. But it is a bit cumbersome and error-prone. You may forget to add a subscription to the array or to unsubscribe to it in "ngOnDestroy".
The destroy-subject-Pattern
A more modern pattern is to use a "destroy$" Observable, which emits a value when the component is destroyed. This is a pattern used in some libraries as well.
@Component({
selector: 'app-subject-example-component',
standalone: true,
imports: [],
templateUrl: './subject-example-component.component.html',
styleUrl: './subject-example-component.component.scss'
})
export class SubjectExampleComponentComponent implements OnInit, OnDestroy {
/**
* DI
*/
#activatedRoute = inject(ActivatedRoute);
/** Destroy subject */
#destroy$ = new Subject<void>();
/**
* Angular lifecycle hook to initialize the component
*/
ngOnInit(): void {
this.#activatedRoute.paramMap.pipe(
takeUntil(this.#destroy$),
map(params => params.get('username')),
).subscribe();
}
/**
* Angular lifecycle hook to destroy the component
*/
ngOnDestroy(): void {
this.#destroy$.next();
this.#destroy$.complete();
}
}
What happens is:
- In the "ngOnInit" method, the subscription is created and the Observable "#destroy$" is used to unsubscribe to it via the RxJs-Operator "takeUntil".
- In the "ngOnDestroy" method, the Observable "#destroy$" emits a value, which causes the subscription to be unsubscribed.
This pattern is more concise and less error-prone. But it is still a bit cumbersome and you need to remember to add the "takeUntil" operator to each subscription. And we also have to remember to add the "ngOnDestroy" method.
The'@ngneat/until-destroy'-Pattern
This patter requires the installation of the package "@ngneat/until-destroy". It is a decorator-based pattern, which is very concise and less error-prone. It is currently used in many projects. And it is our working horse in many projects. Save and reliable.
@UntilDestroy()
@Component({
selector: 'app-ngneat-until-destroy-example-component',
standalone: true,
imports: [],
templateUrl: './ngneat-until-destroy-example-component.component.html',
styleUrl: './ngneat-until-destroy-example-component.component.scss'
})
export class NgneatUntilDestroyExampleComponentComponent implements OnInit {
/**
* DI
*/
#activatedRoute = inject(ActivatedRoute);
/**
* Angular lifecycle hook to initialize the component
*/
ngOnInit(): void {
this.#activatedRoute.paramMap.pipe(
untilDestroyed(this),
map(params => params.get('username')),
).subscribe();
}
}
What happens is:
- The component is decorated with "@UntilDestroy()".
- In the "ngOnInit" method, the subscription is created. Within the RxJs pipeline, the RxJs-Operator "untilDestroyed(this)" is used to unsubscribe to it.
- No "ngOnDestroy" method is needed.
This pattern is the most concise and less error-prone. It is our preferred pattern in many projects. And we really like it.
The Angular takeUntilDestroyed-Pattern
This pattern is a bit different. It is a pattern, which is not based on a package, but on the Angular-Core. It started with Angular v17 and is based on the new Signal-Concept of Angular. It is a bit more demanding, but a nice alternative to the "@ngneat/until-destroy"-Pattern.
@Component({
selector: 'app-take-until-destroyed-example-component',
standalone: true,
imports: [],
templateUrl: './take-until-destroyed-example-component.component.html',
styleUrl: './take-until-destroyed-example-component.component.scss'
})
export class TakeUntilDestroyedExampleComponentComponent implements OnInit {
/**
* DI
*/
#destroyRef = inject(DestroyRef);
#activatedRoute = inject(ActivatedRoute);
/**
* Angular lifecycle hook to initialize the component
*/
ngOnInit(): void {
this.#activatedRoute.paramMap.pipe(
takeUntilDestroyed(this.#destroyRef),
map(params => params.get('username')),
).subscribe();
}
}
What happens is:
- The component receives 'destroyRef' from "DestroyRef" from "@angular/core" via dependency injection.
- In the "ngOnInit" method, the subscription is created. Within the RxJs pipeline, the RxJs-Operator "takeUntilDestroyed(this.#destroyRef)" is used to unsubscribe to it.
- No "ngOnDestroy" method is needed.
- All sounds good, but there is a caveat. If the Observable monitored has been delayed by e.g. the delay-Operator of RxJs or
setTimeout
, it may cause the errorNG0911: View has already been destroyed
. This is difficult to track, since the error cause may be in libraries used. We monitored this issue e.g. inregisterOnChange
methods of CVA-Components. If you encounter this issue, you should use the "@ngneat/until-destroy"-Package or explicit subscription management.
Conclusion
We think that the "@ngneat/until-destroy"-Pattern is currently the best pattern to automatically unsubscribe to Observables in Angular. It is concise and less error-prone. And it is our preferred pattern in many projects. In future, the Angular takeUntilDestroyed-Pattern may be a good alternative. It is based on the Angular-Core and does not require an additional package.
Thanks for reading.