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 error NG0911: 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. in registerOnChange 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.