The Angular team just released an exciting new major version of Angular that comes with many new features. One of these features has been something that the Angular community has wanted for a long time, and we've inched closer and closer with each major version. I'm talking about the takeUntilDestroyed
operator.
Wait... Is This More Important Than Signals?
Maybe this feature isn't as impactful as Signals, but takeUntilDestroyed()
solves a problem that the Angular community has trying to solve for years: what is the "cleanest" pattern for preventing memory leaks when using RxJS?
Want to learn more about preventing memory leaks and other must-knows for optimizing your Angular application's performance? Check out our blog post that lists 5 Essential Tips to Improve Angular App Performance
For a long time, the Angular community has been split on what is the best way to deal with cleaning up subscriptions when using RxJS. When the async
pipe is out of the question, there are two popular solutions:
-
Using the
unsubscribe()
or SubSink pattern -
Using the
takeUntil()
pattern
Both solutions look somewhat similar and have led to many debates over the years.
The unsubscribe() or SubSink Pattern
The "unsubscribe" or SubSink pattern is the concept of storing a reference to all subscriptions and using the unsubscribe
method in the OnDestroy
lifecycle hook:
import { Component } from '@angular/core';
import { Subscription, Observable } from 'rxjs';
@Component({/* ... */})
export class MyComponent {
foo$: Observable<Foo> = this.bar();
subscriptions = new Subscription();
constructor() {
this.subscriptions.add(this.foo$.subscribe());
}
ngOnDestroy(): void {
this.subs.unsubscribe();
}
}
Although this pattern works as expected, usage of the SubSink pattern tends to draw controversy for not being declarative, which is why developers have argued to use the takeUntil
pattern.
The takeUntil() Pattern
The "takeUntil" pattern is the concept of creating a Subject and using the takeUntil
operator to complete subscriptions by calling next()
and following up with complete()
to clean up the Subject itself in the OnDestroy lifecycle hook:
import { Component } from '@angular/core';
import { takeUntil, Observable, Subject } from 'rxjs';
@Component({/* ... */})
export class MyComponent {
foo$: Observable<Foo> = this.bar();
unsubscribe$ = new Subject<void>();
constructor() {
this.foo$.pipe(
takeUntil(this.unsubscribe$)
).subscribe();
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
Despite both patterns working as expected, developers have been in search of a way to avoid having to use the OnDestroy lifecycle hook in hopes of less boilerplate in their code.
This inspiration birthed many libraries such as ngx-auto-unsubscribe and @ngneat/until-destroy which utilizes decorators. While these solutions are valid, they either still require the OnDestroy
lifecycle hook or required knowledge of how decorators might interfere with Angular's internal decorators.
Everything Changed When Angular 14 Released
When Angular 14 released, the inject()
helper function could now be used in components which gave us a way to create a custom RxJS operator for cleaning up subscriptions:
import { inject } from '@angular/core';
export function takeUntilDestroyed<T>(): UnaryFunction<Observable<T>, Observable<T>> {
const viewRef = inject(ChangeDetectorRef) as ViewRef;
const unsubscribe$ = new ReplaySubject<void>(1);
viewRef.onDestroy(() => {
unsubscribe$.next();
unsubscribe$.complete();
});
return (observable: Observable<T>) => observable.pipe(takeUntil(unsubscribe$));
}
Now developers had a simple way to avoid the OnDestroy lifecycle hook without needing custom decorators:
import { Component } from '@angular/core';
import { takeUntilDestroyed } from '../operators/take-until-destroyed';
import { Observable } from 'rxjs';
@Component({/* ... */})
export class MyComponent {
foo$: Observable<Foo> = this.bar();
constructor() {
this.foo$.pipe(
takeUntilDestroyed()
).subscribe();
}
}
This was great! And so far was the "cleanest" way to manage subscriptions. The only issue now was that developers had to write this custom RxJS operator in their codebase or import a library that implemented it... until Angular 16 was released.
Now takeUntilDestroyed() Comes Out of the Box
With the release of Angular 16, the takeUntilDestroyed
operator is now shipped with Angular and is a fully documented feature of Angular as part of the RxJS Interop package developer preview:
import { Component } from '@angular/core';
import { takeUntilDestroyed } from '../operators/take-until-destroyed';
import { Observable } from 'rxjs';
@Component({/* ... */})
export class MyComponent {
foo$: Observable<Foo> = this.bar();
constructor() {
this.foo$.pipe(
takeUntilDestroyed()
).subscribe();
}
}
Conclusion
This might not be the flashiest feature to come to Angular, but it is something that has been requested for many major versions. It's great to see that Angular is improving with each major version.
What Do You Think?
Is your team stuck on an older version of Angular and missing out on all these features? Don't hesitate to schedule a free consultation call if you need any assistance upgrading to Angular 16. Our team is always here to support you.
Maybe you have a better pattern for preventing memory leaks when using RxJS. Join our Community Discord and share your feedback with us on the #Angular channel.