Angular uses RxJS Observables quite extensively for asynchronous data anything from HTTP requests, form value changes, events, route parameters, and many more. Most of the time, you would subscribe to them, but not unsubscribing properly may cause memory leaks and unexpected behavior, especially in large or long-running apps. In this article, we share how we faced a real-life issue related to missed unsubscriptions, how we identified the leak, and how we applied best practices, such as the takeUntil operator and a reusable base class.

Real Scenario
In various dashboard applications, several components had the possibility of listening to data streams coming from the API, user interactions, and changes in route parameters. Such components use subscription on observables through the RxJS subscribe() method inside the Angular lifecycle hooks of ngOnInit().

Example
ngOnInit(): void {
  this.route.params.subscribe(params => {
    this.loadData(params['id']);
  });

  this.userService.getUser().subscribe(user => {
    this.user = user;
  });
}


After navigating between routes multiple times, we noticed the following issues.

  • Console logs appeared multiple times for the same action.
  • Network requests were duplicated.
  • The browser’s memory usage slowly increased over time.

Root Cause

  • Upon inspection using Chrome DevTools memory tab and Angular DevTools, we found that components were not being garbage collected. This was due to active subscriptions holding references to destroyed components.
  • Solution: Using the takeUntil Pattern with Subject.

To fix this, we implemented the takeUntil pattern with a private Subject.

Step 1. Declare an Unsubscribe Subject.
private destroy$ = new Subject<void>();

Step 2. Use takeUntil(this.destroy$) in Every Subscription.
ngOnInit(): void {
  this.route.params
    .pipe(takeUntil(this.destroy$))
    .subscribe(params => this.loadData(params['id']));

  this.userService.getUser()
    .pipe(takeUntil(this.destroy$))
    .subscribe(user => this.user = user);
}


Step 3. Emit and complete the Subject in ngOnDestroy().
ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}

This pattern ensures that all subscriptions automatically unsubscribe when the component is destroyed.

Improvement: Create a Base Component Class

To avoid repeating the same code in every component, we created a base class.
export abstract class BaseComponent implements OnDestroy {
  protected destroy$ = new Subject<void>();

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}


Now, in any component.
export class MyComponent extends BaseComponent implements OnInit {

  ngOnInit(): void {
    this.dataService.getData()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => this.data = data);
  }

}


Alternative Approach: AsyncPipe for Simpler Cases
In cases where we can bind observables directly in the template, we prefer using Angular’s AsyncPipe, which handles subscription and unsubscription automatically.

Instead of,
this.dataService.getData().subscribe(data => {
  this.data = data;
});


Use in the template.
data$ = this.dataService.getData();

<div *ngIf="data$ | async as data">
  {{ data.name }}
</div>


Conclusion
Failing to unsubscribe from observables in Angular can lead to performance issues, duplicate API calls, and memory leaks. Using takeUntil with a Subject is a reliable and scalable solution, especially when combined with a base component class. For simpler use cases, Angular's AsyncPipe provides a clean and safe way to handle subscriptions in templates. Adhering to these practices keeps your Angular applications running smoothly, easy to maintain, and protected from those memory leaks that can improve performance. You will maintain both efficiency and code clarity as a result.