DEV Community

Cover image for Avoid Memory Leaks in Angular
Theoklitos Bampouris
Theoklitos Bampouris

Posted on

Avoid Memory Leaks in Angular

Originally published at https://www.bampouris.eu/blog/avoid-memory-leaks-angular


Almost five years ago, Ben Lesh wrote a nice article with title: RxJS: Don’t Unsubscribe. The author of course doesn't tell us to never care about our Subscription. He means that we must find a way that we don't have to perform .unsubscribe() manually in each one. Let's start our mission!

Our Road Map

The lifetime of some global components, such as AppComponent, is the same as the lifetime of the app itself. If we know that we're dealing with such a case it is acceptable to .subscribe() to an Observable without providing any memory leak guard step. However, handle memory leaks during the implementation of an Angular application is a critical task for every developer. We'll begin our quest with showing what we mean with memory leak and we'll proceed solving the problem at first with the "traditional" way of .unsubscribe(), until we explore our preferable pattern.

The Bad Open Subscriptions

We have a simple demo app with two routing components: FirstComponent and SecondComponent (First Cmp and Second Cmp nav link buttons respectively). The FirstComponent (corresponding to path /first) subscribes to a timer1$ observable and sends messages to a ScreenMessagesComponent via a MessageService. The messages are displayed at the bottom of the screen.

Live Example

export class FirstComponent implements OnInit {
  timer1$ = timer(0, 1000);

  constructor(private messageService: MessageService) {}

  ngOnInit(): void {
    this.timer1$.subscribe((val) =>
      this.messageService.add(`FirstComponent timer1$: ${val}`)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

When we navigate to /second path, FirstComponent has been destroyed. However, we still see outgoing messages from the above subscription. This is happening because we forgot to "close the door behind us": our app has an open Subscription. As we go back and forth we add more and more subscriptions which will close only when the app is closed. We have to deal with Memory Leaks!

memory-leaks-1

Unsubscribe the Old Way

A straightforward way to solve to above problem is to implement the lifecycle hook method ngOnDestroy(). As we read from the official documentation:

...Unsubscribe Observables and detach event handlers to avoid memory leaks...

export class FirstComponent implements OnInit, OnDestroy {
  private timer1$ = timer(0, 1000);

  private subscription: Subscription;

  constructor(private messageService: MessageService) {}

  ngOnInit(): void {
    this.subscription = this.timer1$.subscribe((val) =>
      this.messageService.add(`FirstComponent timer1$: ${val}`)
    );
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

memory-leaks-2

Furthermore, if we have more than one Subscription, we have to do the same job for each of them.

export class FirstComponent implements OnInit, OnDestroy {
  private timer1$ = timer(0, 1000);
  private timer2$ = timer(0, 2500);

  private subscription1: Subscription;
  private subscription2: Subscription;

  constructor(private messageService: MessageService) {}

  ngOnInit(): void {
    this.subscription1 = this.timer1$.subscribe((val) =>
      this.messageService.add(`FirstComponent timer1$: ${val}`)
    );

    this.subscription2 = this.timer2$.subscribe((val) =>
      this.messageService.add(`FirstComponent timer2$: ${val}`)
    );
  }

  ngOnDestroy(): void {
    this.subscription1.unsubscribe();
    this.subscription2.unsubscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

In case we don't have only one or two subscriptions and we want to reduce the number of .unsubscribe() calls, we can create a parent Subscription and add to it the child ones. When a parent subscription is unsubscribed, any child subscriptions that were added to it are also unsubscribed.

Live Example

export class FirstComponent implements OnInit, OnDestroy {
  private timer1$ = timer(0, 1000);
  private timer2$ = timer(0, 2500);

  private subscription = new Subscription();
  constructor(private messageService: MessageService) {}

  ngOnInit(): void {
    this.subscription.add(
      this.timer1$.subscribe((val) =>
        this.messageService.add(`FirstComponent timer1$: ${val}`)
      )
    );

    this.subscription.add(
      this.timer2$.subscribe((val) =>
        this.messageService.add(`FirstComponent timer2$: ${val}`)
      )
    );
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Using a parent Subscription we don't have to care about plenty of properties and we also perform only one .unsubscribe().

The Async Pipe

AsyncPipe kick ass! It has no rival when we want to display data "reactively" in our component's template.

The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.

Live Example

@Component({
  selector: 'app-first',
  template: `
    <p>first component works!</p>
    <p>{{ timer3$ | async }}</p>
  `,
})
export class FirstComponent implements OnInit, OnDestroy {
  ...

  timer3$ = timer(0, 1000);

  ...
}
Enter fullscreen mode Exit fullscreen mode

Using the AsyncPipe there is no need neither to .subscribe() nor to .unsubscribe() manually.

The RxJS Operators

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It has some great operators such as:

We won't stand in each of them. We'll see only the usage of takeUntil operator.

Lets values pass until a second Observable, notifier, emits a value. Then, it completes.

At first, I'd like to mention the dangers as described in this article: RxJS: Avoiding takeUntil Leaks. takeUntil operator has to be (usually) the last operator in the pipe.

If the takeUntil operator is placed before an operator that involves a subscription to another observable source, the subscription to that source might not be unsubscribed when takeUntil receives its notification.

Live Example

export class FirstComponent implements OnInit, OnDestroy {
  ...
  private destroy$ = new Subject<void>();

  constructor(private messageService: MessageService) {}

  ngOnInit(): void {
    this.timer1$
      .pipe(takeUntil(this.destroy$))
      .subscribe(
        (val) => this.messageService.add(`FirstComponent timer1$: ${val}`),
        (err) => console.error(err),
        () => this.messageService.add(`>>> FirstComponent timer1$ completed`)
      );

    this.timer2$
      .pipe(takeUntil(this.destroy$))
      .subscribe(
        (val) => this.messageService.add(`FirstComponent timer2$: ${val}`),
        (err) => console.error(err),
        () => this.messageService.add(`>>> FirstComponent timer2$ completed`)
      );
  }

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

Here, destroy$ is our second Observable (notifier), which emits inside ngOnDestroy() lifecycle hook, triggered that way the completion of our data streams. An advantage to this approach is it actually completes the observable and so the complete() callback is called. When we call .unsubscribe() there’s no way we’ll be notified that the unsubscription happened.

memory-leaks-3

The Drawback

All the above solutions actually solve our problem, however they all have at least one drawback: we have to repeat ourselves in each component by implementing ngOnDestroy() for our purpose. Is there any better way to reduce boilerplate furthermore? Yes, we'll take advantage of takeUntil and Angular's DI mechanism.

The DestroyService

Live Example

First, we'll move the ngOnDestroy() into a service:

import { Injectable, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable()
export class DestroyService extends Subject<void> implements OnDestroy {
  ngOnDestroy() {
    this.next();
    this.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

The FirstComponent both provides the instance of the service (through the providers metadata array) and injects that instance into itself through its constructor:

@Component({
  selector: 'app-first',
  template: `<p>first component works!</p>`,
  providers: [DestroyService],
})
export class FirstComponent implements OnInit {
  ...

  constructor(
    private messageService: MessageService,
    private readonly destroy$: DestroyService
  ) {}

  ngOnInit(): void {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

We have the exact same result as the previous one! We can provide an instance of DestroyService in any component that needs it.

Conclusions

Eventually, I think that the preferable way to manage our RxJS subscriptions is by using takeUntil operator via an Angular service. Some benefits are:

  • Less code
  • Fires a completion event when we kill our stream
  • Less chance to forget .unsubscribe() or .next(), .complete() methods in the ngOnDestroy() implementation

GitHub repo with the examples is available here.

Top comments (2)

Collapse
 
felvct profile image
Felix Vaucourt

Nice job! Was actually thinking of writing something similar but you covered the whole topic pretty well.

Collapse
 
theoklitosbam7 profile image
Theoklitos Bampouris

Thank you very much Felix!