Unsubscribe from Angular/RxJS subscription

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

Since the release of Angular developers are familiarising themselves with Observables. We all know that we need to subscribe to the results of an HTTP request for example by using the .subscribe() method.

Note that there are other ways to get data as well via HTTP in Angular, but those are not being discussed in this article.

Of course, the question that we need to ask ourselves is, when - if at all - should we unsubscribe?

Having an active subscription on a component that is not being used by Angular leads to potential memory leaks. In this case, a memory leak is when something is taking up space in memory even though it's not used and it's not given back to the operating system as free memory space.

In this article, we'll investigate the situations when we need to call unsubscribe as well as take a look at when it's not necessary at all.

Create a Service

Let's first take a look at a sample service. Imagine that we have an Angular service that can greet 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$;
}
}

We can provide a name to the greet() method and we get the Hello there, Tamas! sentence returned to us.

Create components

Let's now go ahead and create two components. One component will be used to add some buttons to do some tests, and we'll also display the content of the second component in there.

<!-- 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}`));
}
}

To sum this up, we have two components. The first one (main component) displays a bunch of buttons as well as an input box to add a name. It calls the greet() method internally which goes out to the Service and calls the greet() method from there and updates the greeting$ variable.

In the second component, on ngOnInit we go to the API and call the getGreeting() method to get the value of greeting$.

In the first component, we have also added a button to destroy the component. Destroying a component is simple, all we need to do is to remove it from the DOM tree, and we can do it via a simple *ngIf and a boolean.

Let's now discuss the problem. Enter a name in the input box, hit the Greet button a few times and you should be presented with the following message in the browser's console: 3 Hello there, Joe! (with the number indicating the number of times the button was clicked)

Let's see what happens when we destroy the component. Click the Destroy component button. Then, try to click the Greet button again. Notice how the console gets populated with a message. Now, this shouldn't happen, right? The component that is responsible for displaying that message is destroyed. The problem is that we didn't unsubscribe from the observable - this is a classic example of a memory leak.

Unsubscribe from an observable

To unsubscribe from an observable, we need to do two things. The first one is to make sure that we call the OnDestroy lifecycle hook and that we also update how the subscription is made in the first place.

We'll be using the takeUntil method that emits values until a provided observable emits. This means that we'll add a "fake" observable, which is going to be the observable that we remove when the component is destroyed. This way we can make sure that the subscription is also terminated.

This is how the updated component looks like:

// 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 this file and rerun the test. Notice that once the component has been destroyed, hitting the greet() button is not going to produce values in the console.

Unsubscribe from an HTTP subscription

Another common way to use observables in Angular applications is by using the HttpClientModule.

Please, note the difference between Http and HttpClient

The question is of course, do we need to unsubscribe from observables when making HTTP requests. The answer is no. Let's assume that we are using an API to gather information about products. Our service would look 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 this is how the updated main component would look like:

// 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 its corresponding HTML template:

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

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

Notice how we are using @ViewChild to access the properties of the Products child component. Clicking the Check Products Subscription button a few times would always return the list of products. However, once the component is destroyed, pressing that button would yield an error - the values are not going to be accessible.

Calling unsubscribe explicitly is not required since under the hood Angular implements the XMLHttpRequest() which is subject to garbage collection once the event listener attached to it (load) is done collecting the data. Therefore unsubscription is automatically done via garbage collection.