The Best Way to Unsubscribe from Angular Observables

angular-infinite-event-loop

The inclusion of RxJS Observable in Angular core is one of the most important additions in Angular’s history. It is also one of the most misused. When used improperly, the Observable can open up memory leaks making your application sluggish or causing it to crash. Today we will talk about how to properly clean up observables and the very best way to do it.

I encourage you to follow along…

Built with

  • Angular v10.0.0

Getting Started

Prerequisites

Get the newest vscode => https://code.visualstudio.com/download

Get the newest LTS node => https://nodejs.org

Installation

  1. Open a terminal where you want the project to be installed

  2. Install the Angular CLI globally

    npm install -g @angular/cli
    
  3. Create a new Angular project

    ng new angular-observable-unsubscribe --interactive=false
    
  4. Navigate into the repository

    cd angular-observable-unsubscribe
    
  5. Open the project in vscode

    code .
    
  6. Pop open the vscode terminal

    CTRL + `

  7. Start up the project

    npm start
    
  8. Open a Chrome browser here.

    http://localhost:4200

    initial-start

The Code

All started up? Great! Now let’s put some of our code in.

  1. Kill the server

    CTRL + C
    
  2. Generate a new component named “count”

    ng generate component count
    
  3. In src/app/count/count.component.html

    <p>{{time}}</p>
    
  4. In src/app/count/count.component.ts

  import { Component, OnInit } from '@angular/core';
  import { interval } from 'rxjs';

  @Component({
    selector: 'count',
    templateUrl: './count.component.html',
    styleUrls: ['./count.component.css']
  })
  export class CountComponent implements OnInit {
    time=0;
    constructor() { }

    ngOnInit(): void {
      interval(1000).subscribe(val=>{
        this.time=val;
        console.log(val);
      })
    }
  }
  1. In src/app/app.component.html

    <button *ngIf="!showCount" (click)="showCount=true">count</button>
    <button *ngIf="showCount" (click)="showCount=false">stop</button>
    <count *ngIf="showCount"></count>
    
  2. In src/app/app.component.ts

    import { Component } from '@angular/core';
    
     @Component({
       selector: 'app-root',
       templateUrl: './app.component.html',
       styleUrls: ['./app.component.css']
     })
     export class AppComponent {
       showCount=false;
     }
    
  3. Start up the project again

    npm start
    

count-no-unsubscribe

Clicking the count button starts the timer and shows the count. Click stop and it stops. Click count again and we get a fresh timer starting at zero.

But wait, what is happening in the console?…

The Memory Leak

Woopsie! When we press stop, the observable subscription is not actually stopped. We can see in the cosole that it keeps on keeping on. What’s worse is that every new timer that is started continues to run forever. This, my friends is what we call a memory leak.

its-fine

You might be able to get away with this if your app is simple seconds counter. But if your app is observing services with heavy payloads, this can easily spiral out of control. Here are three ways to unsubscribe from your Angular observables. Good. Better. and Best.

Good

Manually unsubscribe from each observable.

  1. In src/app/count/count.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Observable, Subscription } from 'rxjs';

@Component({
  selector: 'count',
  templateUrl: './count.component.html',
  styleUrls: ['./count.component.css'],
})
export class CountComponent implements OnInit, OnDestroy {
  time = 0;
  timer$: Subscription;
  constructor() {}

  ngOnInit(): void {
    this.timer$ = interval(1000).subscribe((val) => {
      this.time = val;
      console.log(val);
    });
  }

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

To unsubscribe from an observable subscription, we must create a Subscription variable (timer$), assign the subscription to this variable, and then in the ngOnDestroy lifecycle hook unsubscribe the subscription. This is fine… and it works. This is Observables 101.

count-good

And we can see this is now behaving as expected. But what if you had 100 observables in this component? We would have to make 100 variables and unsubscribe 100 times. If there were only a better way.

Better

Implicitly unsubscribe from observables.

  1. In src/app/count/count.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'count',
  templateUrl: './count.component.html',
  styleUrls: ['./count.component.css'],
})
export class CountComponent implements OnInit, OnDestroy {
  time = 0;
  private unsubscribe$ = new Subject();
  constructor() {}

  ngOnInit(): void {
    interval(1000)
      .pipe(takeUntil(this.unsubscribe$))
      .subscribe((val) => {
        this.time = val;
        console.log(val);
      });
  }

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

In this case we create an unsubscribe$ Subject. In the observable the RxJS takeUntil operator is passed the subject and will remain open until the subject is completed. In ngOnDestroy, we call the next item if one exists and then completes.

This is a bit more complicated to understand than the first example, but has a nice benefit. We no longer need to create a variable for each observable and no longer need to unsubscribe in the destroy.

Best

If you only need to read from your observable, the best way to do so is with the async pipe.

  1. In src/app/count/count.component.ts
import { Component } from '@angular/core';
import { interval } from 'rxjs';

@Component({
  selector: 'count',
  templateUrl: './count.component.html',
  styleUrls: ['./count.component.css'],
})
export class CountComponent {
  timer$ = interval(1000);
}
  1. In src/app/count/count.component.html
<p>{{(timer$ | async)}}</p>

With an observable declared in the typescript, we can simply reference the observabe in the markup and apply the async pipe. This pipe handles the subscription internally and there is no need to remember to unsubscribe as this pipe handles that for you.

Conclusion

In conclusion, all of these are perfectly acceptable ways to unsubscribe from observables. It is up to you to determine the right one for your situation.

Written on June 30, 2020