<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Frontend development
Loading

Angular |

Angular 16 Just Dropped a Long Overdue Feature

Angular 16 features a new takeUntilDestroyed operator, solving the long-standing challenge of preventing memory leaks in RxJS subscriptions.

Mark Thompson

Mark Thompson

Twitter Reddit

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:

  1. Using the unsubscribe() or SubSink pattern
  2. 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.

Join our Discord