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.
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.
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.
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.
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.