Build the future of communications.
Start building for free

Preventing Memory Leaks in Angular Observables with ngOnDestroy

UvUUs1WXEBgwWcMbhbQ_JB5tScafJWbz95oNsfYnIyQQWW0oHawdrojZMGqEuHSiDAjdGZ8PJgMqAF_BPBopvBQL5c-wBaV38wRXUX4H0P7b57cflMV53rxc9q_G7TqZ58I9b0Fv

A memory leak is one of the worst types of issues you can have. It’s hard to find, hard to debug, and often hard to solve. Unfortunately, this problem occurs in every programming language or framework, including Angular. Observables are awesome, because of the incessant stream of data, but this benefaction can cause serious problem with memory leak. Today we will take a closer look at the ngOnDestroy Angular hook, and answer the question: “When should I unsubscribe from an observable? What is the best pattern to use?”

In this post we will:

  • Create an app which generates random numbers.
  • Reproduce a memory leak in it.
  • Fix the memory leak with the takeUntil + ngOnDestroy pattern.

To accomplish these tasks, make sure you:

  • Install Node.js and npm (at the time of writing I am using Node.js v8.11.1 and npm 5.8.0).
  • Install @angular/cli (in this post I am using version 6.0.0).
  • Have intermediate knowledge of Angular.

Let’s Create the App

We need to start by initializing a new Angular project. To do that, enter the following commands in your terminal:

ng new memoryLeakApp
cd memoryLeakApp

The ng new command will initialize a new Git repository and commit the project for you.

Now create the following directories and files under the memoryLeakApp directory. We will edit them later:

mkdir src/app/lucky
touch src/app/lucky/lucky.component.ts
touch src/app/lucky/lucky.service.ts
mkdir src/app/really
touch src/app/really/really.component.ts

OK, time to start coding. Start by creating LuckyService, which will generate random numbers and push them to the observable returned by the getLuckyNumber method. For debugging purposes, we will also implement thegetSubscribersCount method, which will return the number of clients who subscribed to the observable. Place this code in src/app/lucky/lucky.service.ts:

import { Observable, Subject } from 'rxjs;

export class LuckyService {
 private luckyGenerator$: Observable<number>;
 private subscribersCount = 0;

 public getLuckyNumber(): Observable<number> {
   this.subscribersCount++;

   if (!this.luckyGenerator$) {
     this.luckyGenerator$ = Observable.create((subject: Subject<number>) => {
       setInterval(() => {
         const number = Math.floor(Math.random() * 10);
         subject.next(number);
       }, 1000);
     });
   }
   return this.luckyGenerator$;
 }

 public getSubscribersCount(): number {
   return this.subscribersCount;
 }
}

Now we have something that we can consume in the component. Place the following code in src/app/lucky/lucky.component.ts:

import { Component, OnInit } from '@angular/core';
import { LuckyService } from './lucky.service';

@Component({
 template: `
   <p>You are checking if you are lucky {{displayCount}} time</p>

   <p>Your lucky number is: {{number}}</p>
 `,
})
export class LuckyComponent implements OnInit {
 public subscribersCount = 0;
 public number: number;

 constructor(private luckyService: LuckyService) {}

 public ngOnInit(): void {
   this.luckyService.getLuckyNumber().subscribe((luckyNumber: number) => {
     this.number = luckyNumber;
     console.log('Retrieved lucky number ${this.number} for subscriber ${this.subscribersCount}');
   });
   this.subscribersCount = this.luckyService.getSubscribersCount();
 }
}

Now we are going to create another component. This is necessary to reproduce the memory leak in our app. Place this code into src/app/really/really.component.ts:

import { Component } from '@angular/core';

@Component({
 template: `<p>Am I really lucky? Let's check that in console...</p>`,
})
export class ReallyComponent {}

Update NgModule declaration in the src/app/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { LuckyComponent } from './lucky/lucky.component';
import { LuckyService } from './lucky/lucky.service';
import { ReallyComponent } from './really/really.component';
import { RouterModule } from '@angular/router';

@NgModule({
 declarations: [
   AppComponent,
   LuckyComponent,
   ReallyComponent
 ],
 imports: [
   BrowserModule,
   RouterModule.forRoot([
     { path: '', redirectTo: 'lucky', pathMatch: 'full'},
     { path: 'lucky', component: LuckyComponent, pathMatch: 'full'},
     { path: 'really', component: ReallyComponent, pathMatch: 'full'},
   ])
 ],
 providers: [
   LuckyService
 ],
 bootstrap: [AppComponent]
})
export class AppModule { }

As the last step, we need to modify src/app/app.component.html and replace the code generated by the CLI with our routes:

<div class="navigation">
 <a routerLink="/lucky">Lucky component!</a>&nbsp;
 <a routerLink="/really">Am I really lucky?</a>
</div>
<router-outlet></router-outlet>

You’ll find all the code up to this point in a GitHub repository, which you can clone:

git clone -b angular_memory_leak_step1 https://github.com/maciejtreder/angular-memory-leak.git memoryLeakApp
cd memoryLeakApp/
npm install

Time for the Memory Leak

Let’s run the app and see how it’s working:

ng serve

Navigate to http://localhost:4200 with your browser and you should see:

Yay, the app is working. Random numbers are displayed after every second. But...when you navigate to the second component, take a look at what’s going on there:

As you can see in the console, the subscription made in LuckyComponent stays alive and is still executing the code. What will happen if we navigate to the LuckyComponent again? Or if we navigate multiple times there and back? We will have a serious problem:

Unsubscribe Using ngOnDestroy

What can we do? The ngOnDestroy lifecycle hook looks like a natural candidate for getting rid of our issue. Let’s implement it, and unsubscribe from the observable inside. Change src/app/lucky/lucky.component.ts accordingly:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { LuckyService } from './lucky.service';
import { Subscription } from 'rxjs’;

@Component({
 template: `
   <p>You are checking if you are lucky {{displayCount}} time</p>
   <p>Your lucky number is: {{number}}</p>
`,
})
export class LuckyComponent implements OnInit, OnDestroy {
 public subscribersCount: number = 0;
 public number: number;

 private luckySubscription$: Subscription;

 constructor(private luckyService: LuckyService) {}

 public ngOnInit(): void {
   this.luckySubscription$ = this.luckyService.getLuckyNumber().subscribe((luckyNumber: number) => {
     this.number = luckyNumber;
     console.log('Retrieved lucky number ${this.number}, for subscriber ${this.subscribersCount}');

   });
   this.subscribersCount = this.luckyService.getSubscribersCount();
 }

 public ngOnDestroy(): void {
   this.luckySubscription$.unsubscribe();
 }
}

OK, this looks legit. What we did here is unsubscribe from our observable in the ngOnDestroy lifecycle hook, which will be executed whenever the component is destroyed, which happens when the page is left by the visitor. Let’s re-run the application and check if the memory leak is gone:

ng serve

After navigating away from `LuckyComponent`, you should see that the code from subscriptions is no longer executed. The observable has been unsubscribed inside the `ngOnDestroy` hook. Great! Let’s navigate multiple times between components and make sure that everything works as expected, and we are unsubscribing each time.

You’ll find all the code up to this point in this GitHub repository, which you can clone:

git clone -b angular_memory_leak_step2 https://github.com/maciejtreder/angular-memory-leak.git memoryLeakApp
cd memoryLeakApp/
npm install

TakeUntil Pattern

That worked well for one observable, but what if we have multiple observables, which we need to unsubscribe manually? Do we need to add multiple Subscription variables, just to unsubscribe them in ngOnDestroy? Does our code need to look like this:

export class LuckyComponent implements OnInit, OnDestroy {
 public number1: number;
 public number2: number;

 private luckySubscription1: Subscription;
 private luckySubscription2: Subscription;
 private luckySubscription3: Subscription;
 private luckySubscription4: Subscription;
 private luckySubscription5: Subscription;
 private luckySubscription6: Subscription;

 constructor(private luckyService: LuckyService) {}

 public ngOnInit(): void {
   /*
      subscribing to multiple observables comes here
  */
 }

 public ngOnDestroy(): void {
   this.luckySubscription1.unsubscribe();
   this.luckySubscription2.unsubscribe();
   this.luckySubscription3.unsubscribe();
   this.luckySubscription4.unsubscribe();
   this.luckySubscription5.unsubscribe();
   this.luckySubscription6.unsubscribe();
 }
}

Of course not. We can do it in a much cleaner way. This is where takeUntil comes into play. Here’s what the documentation says about the takeUntil method:

Returns the values from the source observable sequence until the other observable sequence or Promise produces a value.

The most important part of this definition is: until the other observable … produces a value.

OK, so we need that “other observable”. Copy this code into src/app/lucky/lucky.component.ts:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { LuckyService } from './lucky.service';
import { Subject } from 'rxjs;
import { takeUntil } from 'rxjs/operators';

@Component({
 template: `
  <p>You are checking if you are lucky {{displayCount1}} time</p>

  <p>Your lucky number is: {{number1}}</p>
  <p>Another lucky number is: {{number2}}</p>
 `
})
export class LuckyComponent implements OnInit, OnDestroy {
 public number1: number;
 public number2: number;

 private onDestroy$: Subject<void> = new Subject<void>();

 constructor(private luckyService: LuckyService) {}

 public ngOnInit(): void {
   const subscriberCount1 = this.luckyService.getSubscribersCount();
   this.luckyService.getLuckyNumber()
     .pipe(takeUntil(this.onDestroy$))
     .subscribe((luckyNumber: number) => {
       this.number1 = luckyNumber;
       console.log('Retrieved lucky number ${this.number1} for subscriber ${subscriberCount1}');
   });

   const subscriberCount2 = this.luckyService.getSubscribersCount();
   this.luckyService.getLuckyNumber()
     .pipe(takeUntil(this.onDestroy$))
     .subscribe((luckyNumber: number) => {
       this.number2 = luckyNumber;
       console.log('Retrieved lucky number ${this.number2} for subscriber ${subscriberCount2}');
   });
 }

 public ngOnDestroy(): void {
   this.onDestroy$.next();
 }
}

What we have actually done is declare a new observable:

private onDestroy$: Subject<void> = new Subject<void>();

Then, by using pipe method with takeUntil we inform compiler that we want to unsubscribe from the observable when any value appear in onDestroy$:

this.luckyService.getLuckyNumber()
 .pipe(takeUntil(this.onDestroy$))
 .subscribe((luckyNumber: number) => {
   this.number1 = luckyNumber;
   console.log('Retrieved lucky number ${this.number1}, for subscriber ${subscriberCount1}');
});

Finally, we pushed value to the `onDestroy$` inside the `ngOnDestroy` hook:

public ngOnDestroy(): void {
 this.onDestroy$.next();
}

Let’s run the app again, and navigate multiple times between components.

That’s it! Observables are unsubscribed, there is no memory leak, and we did all of that with a couple lines of code.

Summary

Today we learned how we could accidentally run into a memory leak in Angular. Then we applied two possible solutions. (I definitely recommend takeUntil.)

I hope that this post was helpful for you and you will start to use your new knowledge in your one-in-a-million app.

Take a look at the GitHub repository for Step 3 and check out ng-toolkit to learn more Angular and SPA features.

I'm Maciej Treder and you can reach me at contact@maciejtreder.com, https://www.maciejtreder.com and @maciejtreder (GitHub, Twitter, LinkedIn).

Authors
Sign up and start building
Not ready yet? Talk to an expert.