Theoklitos Bampouris
Posted on March 25, 2021
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
- Unsubscribe the Old Way
- The Async Pipe
- The RxJS Operators
- The DestroyService
- Conclusions
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.
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}`)
);
}
}
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!
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();
}
}
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();
}
}
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.
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();
}
}
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.
@Component({
selector: 'app-first',
template: `
<p>first component works!</p>
<p>{{ timer3$ | async }}</p>
`,
})
export class FirstComponent implements OnInit, OnDestroy {
...
timer3$ = timer(0, 1000);
...
}
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 whentakeUntil
receives its notification.
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();
}
}
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.
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
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();
}
}
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 {
...
}
}
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 thengOnDestroy()
implementation
GitHub repo with the examples is available here.
Posted on March 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.