Skip to main content

Unsubscribe from Angular/RxJS subscription

4 min read

Older Article

This article was published 8 years ago. Some information may be outdated or no longer applicable.

Angular developers have been getting comfortable with Observables. We all know we subscribe to HTTP results using .subscribe().

There are other ways to pull data via HTTP in Angular, but they’re not covered here.

The question that matters: when (if ever) should we unsubscribe?

An active subscription on a component that Angular’s no longer using creates a memory leak. Something’s chewing through memory even though nothing needs it, and that memory never gets handed back to the OS.

Let’s look at when you need to call unsubscribe and when you can skip it entirely.

Create a Service

First, a sample service. Imagine an Angular service that greets users:

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

@Injectable()
export class ApiService {
  private greeting$ = new Subject();

  constructor() {}

  greet(name) {
    this.greeting$.next(`Hello there, ${name}!`);
  }
  getGreeting() {
    return this.greeting$;
  }
}

Pass a name to the greet() method and you get back Hello there, Tamas!.

Create components

Now let’s build two components. One holds the buttons and input for testing. It also displays the second component.

<!-- Main Component -->
<button (click)="destroy()" *ngIf="displayed">Destroy component</button>
<br />
<button (click)="greet(field.value)">Greet!</button>
<input type="text" #field id="name" placeholder="Your name" />

<app-second *ngIf="displayed"></app-second>
// Main Component
import { Component } from '@angular/core';
import { ApiService } from '../api.service';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css'],
})
export class MainComponent {
  private displayed = true;
  constructor(private api: ApiService) {}

  greet(value) {
    this.api.greet(value);
  }
}

// Second Component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService } from '../api.service';

@Component({
  selector: 'app-second',
  templateUrl: './second.component.html',
  styleUrls: ['./second.component.css'],
})
export class SecondComponent implements OnInit {
  constructor(private api: ApiService) {}

  ngOnInit() {
    this.api.getGreeting().subscribe((message) => console.log(`${message}`));
  }
}

Quick summary: the main component shows buttons and an input box. It calls greet() internally, which hits the service and updates greeting$. The second component grabs the greeting value on ngOnInit.

The main component also has a button to destroy the second component. Destroying it is simple: yank it from the DOM with *ngIf and a boolean.

Here’s where things go wrong. Type a name, hit Greet a few times. You’ll see something like 3 Hello there, Joe! in the console (the number showing how many times you clicked).

Now click Destroy component. Then hit Greet again. Watch the console. Messages keep appearing. That shouldn’t happen. The component responsible for displaying those messages is gone. The problem: we never unsubscribed from the observable. Classic memory leak.

Unsubscribe from an observable

Two things need to happen. First, implement the OnDestroy lifecycle hook. Second, change how the subscription works.

We’ll use takeUntil, which emits values until a provided observable fires. We create a “trigger” observable that we complete when the component gets destroyed. This tears down the subscription cleanly.

Here’s the updated component:

// Second Component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService } from '../api.service';
import 'rxjs/add/operator/takeUntil';
import { Subject } from 'rxjs/Subject';

@Component({
  selector: 'app-second',
  templateUrl: './second.component.html',
  styleUrls: ['./second.component.css'],
})
export class SecondComponent implements OnInit, OnDestroy {
  private unsub: Subject<any> = new Subject();

  constructor(private api: ApiService) {}
  ngOnInit() {
    this.api
      .getGreeting()
      .takeUntil(this.unsub)
      .subscribe((message) => console.log(`${message}`));
  }

  ngOnDestroy() {
    this.unsub.next();
    this.unsub.complete();
  }
}

Save and rerun the test. Once the component’s destroyed, hitting Greet produces nothing in the console. Exactly what we want.

Unsubscribe from an HTTP subscription

Another common observable pattern in Angular: HttpClientModule.

Note the difference between Http and HttpClient.

Do we need to unsubscribe from HTTP observables? No. Say we’re hitting an API for product data. The service looks like this:

// service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class ApiService {
  constructor(private http: HttpClient) {}

  getProducts() {
    return this.http.get('http://localhost:3000/api/products');
  }
}

And the updated main component:

// Main Component
import { Component, ViewChild } from '@angular/core';
import { ApiService } from '../api.service';
import { ProductsComponent } from '../products/products.component';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css'],
})
export class MainComponent {
  private displayed = true;
  @ViewChild(ProductsComponent) private productChild: ProductsComponent;
  constructor(private api: ApiService) {}

  destroy() {
    this.displayed = false;
  }

  checkProductsSubscription() {
    console.log(this.productChild.products);
  }
}

With the corresponding template:

<!-- Main Component -->
<button (click)="destroy()" *ngIf="displayed">Destroy component</button>
<button (click)="checkProductsSubscription()">
  Check Products Subscription
</button>

<app-products *ngIf="displayed"></app-products>

We’re using @ViewChild to reach into the Products child component’s properties. Click Check Products Subscription a few times and you’ll always get the product list back. But once the component’s destroyed, that button throws an error. The values aren’t accessible anymore.

You don’t need to call unsubscribe explicitly here. Under the hood, Angular uses XMLHttpRequest(), which gets garbage collected once its load event listener finishes collecting data. Unsubscription happens automatically through garbage collection.