DEV Community

Cover image for Efficiently Destroying Observables in Angular
David
David

Posted on • Edited on

Efficiently Destroying Observables in Angular

Managing subscriptions to observables is crucial when working with Angular to prevent memory leaks and ensure the application remains performant. A common mistake developers make (myself included - that is why I'm doing this post) is failing to unsubscribe from observables when a component is destroyed. This blog post will guide you through an efficient way to handle this using Angular's ngOnDestroy lifecycle hook and the takeUntil operator from RxJS.

Why Do You Need to Unsubscribe?

When you subscribe to an observable, it continues to emit values indefinitely unless it completes or you explicitly unsubscribe. If you don't unsubscribe—especially in components that are frequently created and destroyed—you risk memory leaks and unintended behavior, as these observables will keep running in the background even after the component is gone.

Solution 1: takeUntil and ngOnDestroy (Traditional Approach)

The takeUntil operator allows you to automatically unsubscribe from observables when a certain condition is met. By combining this with Angular's ngOnDestroy lifecycle hook, you can ensure that all subscriptions are properly cleaned up when the component is destroyed.

Step-by-Step Implementation

  • Import Necessary Modules: Import Subject from rxjs and takeUntil from rxjs/operators.
  • Create a Subject to Act as a Notifier: This subject will emit a value when the component is destroyed.
  • Use the takeUntil Operator in Your Subscriptions: This ensures that the subscription is automatically unsubscribed when the notifier emits a value.
  • Trigger the Notifier in ngOnDestroy: When the component is destroyed, emit a value from the notifier and complete it.
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-sample',
  templateUrl: './modal-material.component.html',
  styleUrls: ['./modal-material.component.css']
})
export class SampleComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  initializeForm(): void {
    const request: SomeRequest = { /* request data */ };
    this.service.create(request)
      .pipe(takeUntil(this.destroy$))
      .subscribe(
        () => this.finish(),
        error => this.finish(error)
      );
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • destroy$ Subject: This subject will emit a value when the component is destroyed, signaling all subscriptions to complete.
  • takeUntil(this.destroy$): This operator ensures that the subscription is automatically unsubscribed when the destroy$ subject emits a value.
  • ngOnDestroy Lifecycle Hook: When the component is destroyed, the destroy$ subject emits a value and completes, effectively cleaning up all subscriptions that use takeUntil(this.destroy$).

Solution 2 takeUntilDestroyed (Modern Approach - Angular 16+)

Angular 16 introduced the takeUntilDestroyed operator, which simplifies the subscription cleanup process by automatically handling the destruction logic without requiring manual implementation of ngOnDestroy or creating a destroy subject.

Step-by-Step Implementation

  • Import the Operator: Import takeUntilDestroyed from @angular/core/rxjs-interop.
  • Use in Your Subscriptions: Simply pipe your observables through takeUntilDestroyed().
  • Injection Context: Ensure the operator is used within an injection context (constructor, field initializer, or factory function).
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-sample',
  templateUrl: './modal-material.component.html',
  styleUrls: ['./modal-material.component.css']
})
export class SampleComponent {
  private service = inject(SomeService);

  initializeForm(): void {
    const request: SomeRequest = { /* request data */ };
    this.service.create(request)
      .pipe(takeUntilDestroyed())
      .subscribe(
        () => this.finish(),
        error => this.finish(error)
      );
  }
}
Enter fullscreen mode Exit fullscreen mode

Alternative Usage with DestroyRef

If you need more control or are using the operator outside of the injection context, you can manually inject DestroyRef:

import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-sample',
  templateUrl: './modal-material.component.html',
  styleUrls: ['./modal-material.component.css']
})
export class SampleComponent {
  private destroyRef = inject(DestroyRef);
  private service = inject(SomeService);

  initializeForm(): void {
    const request: SomeRequest = { /* request data */ };
    this.service.create(request)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(
        () => this.finish(),
        error => this.finish(error)
      );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Advantages of takeUntilDestroyed:

  • Less Boilerplate: No need to implement OnDestroy or manage a destroy subject.
  • Automatic Cleanup: Automatically handles the destruction logic.
  • Type Safety: Better integration with Angular's dependency injection system.
  • Modern Approach: Leverages Angular's latest patterns and best practices.

Which Approach Should You Use?

  • Use takeUntilDestroyed if you're using Angular 16+ and want a cleaner, more modern approach with less boilerplate code.
  • Use takeUntil with ngOnDestroy if you're working with older Angular versions or need more granular control over the destruction process.

Conclusion

Managing observable subscriptions is essential for building performant Angular applications. Whether you choose the traditional takeUntil approach or the modern takeUntilDestroyed operator, both methods effectively prevent memory leaks by ensuring subscriptions are properly cleaned up when components are destroyed.

The takeUntilDestroyed operator represents Angular's evolution toward more developer-friendly APIs that reduce boilerplate while maintaining the same level of functionality. Consider upgrading to this approach if you're using Angular 16 or later.

Implement these patterns in your Angular projects to ensure clean and efficient resource management, leading to a smoother and more reliable user experience. Happy coding!

Top comments (2)

Collapse
 
mthood profile image
mthood • Edited

hi, i use takeuntildestroyed() that simplify all things. This approach in obsolete is my projects. Thanks,

Collapse
 
tbeaumont79 profile image
Thibault Beaumont

Thanks for that ! :-)