Unsubscribe from RxJS subscriptions using SubSink
Uzochukwu Eddie Odozi
Posted on June 8, 2019
In this article, we will learn how to use the subsink module to unsubscribe from rxjs subscriptions.
Angular uses RxJS as a backbone of the Angular application. RxJS is a javascript library that brings to us the concept of Reactive Programming to the web. In other words, RxJS is a library that uses observables to make it easier to compose callback base code or asynchronous code RxJS Docs.
RxJS uses the concept of Observables and Observers, where the Observable is the source of data and the Observer uses the data. Each time an Observable produces data, the Observer is notified and the data is handled inside the subscribe method. It is very important to note that if this subscriptions are not handled properly, it can lead to the possibility of memory leaks.
In other to prevent memory leaks due to Observable subscriptions, there are different methods or operators from RxJS that can be used as well as modules.
Let us build a sample angular app and then use the SubSink module for unsubscription.
Application Setup
First, we need to create a new angular project (With assumption that you have the angular cli). Run the command
ng new sub-sink
Install bootstrap 4
npm i bootstrap
Add this to your styles.css file
@import '~bootstrap/dist/css/bootstrap.min.css';
We need two new components and a service. The first component will only subscribe to the Observable while the second component will subscribe to the Observable using SubSink and then it will be unsubscribed in the ngOnDestroy method of the component. The service will contain two simple methods.
To generate a component
ng g component components/card
ng g component components/subsink
The ng generate command creates two components inside a components directory. The components directory is optional.
Next, create the service file
ng g s services/subsink --flat --skipTests=true
The flag options added to the above command will create the service file without its spec.ts and with its own directory.
In the service file, add
private capitalizeFirstLetter$ = new Subject();
constructor() { }
capitalizeFirstLetter(value: string) {
if (typeof value === undefined) {
return;
}
let str = value.charAt(0);
str = str ? str.toUpperCase() + value.slice(1) : '';
this.capitalizeFirstLetter$.next(str);
}
getString() {
return this.capitalizeFirstLetter$;
}
Also, import Subject
import { Subject } from 'rxjs';
The capitalizeFirstLetter
method just converts the first letter of the string to uppercase and then pass the value into the next
method of the Subject (Docs) which is the property capitalizeFirstLetter$
. The getString
method returns the value passed into the Subject's next method.
Add into the app.component.ts file
showCards = true;
constructor(private subsinkService: SubsinkService) {}
capitalizeFirstLetter(value) {
this.subsinkService.capitalizeFirstLetter(value);
}
displayCards() {
this.showCards = true;
}
hideCards() {
this.showCards = false;
}
and also import the subsink service
import { SubsinkService } from './services/subsink.service';
The capitalizeFirstLetter
method gets the method from the subsink service that converts the first letter to uppercase. The displayCards
method shows the cards from the components using the showCards
property while the hideCards
hides the components.
Add these code to the app.component.html template file. It contains a form and the components as children.
<br>
<div class="container" style="text-align: center">
<h1>Angular Subsink Example</h1>
<br>
<div class="row mt-12" >
<form class="form-inline" style="margin: 0 auto; text-align: center">
<div class="form-group mx-sm-3">
<input type="text" class="form-control" #field placeholder="Message">
</div>
<div class="form-group mx-sm-3">
<button type="button" class="btn btn-primary" (click)="capitalizeFirstLetter(field.value)">Capitalize</button>
</div>
<div class="form-group mx-sm-3">
<button type="button" class="btn btn-danger" (click)="hideCards()">Close Card</button>
</div>
<div class="form-group mx-sm-3">
<button type="button" class="btn btn-success" (click)="displayCards()">Open Card</button>
</div>
</form>
</div>
<br>
<div class="row mt-5">
<app-card *ngIf="showCards"></app-card>
</div>
<div class="row mt-5">
<app-subsink *ngIf="showCards"></app-subsink>
</div>
</div>
<router-outlet></router-outlet>
The template consist of a form with an input and three buttons. The first button calls the capitalizeFirstLetter
method, the second button is for hiding the cards and the third button is for displaying the cards. By default, the cards are displayed.
Now to use the getString
method inside the card component, add to the card.component.ts file and also import the subsink service file
name = 'No unsubscribe component';
message: string;
constructor(private subsinkService: SubsinkService) { }
ngOnInit() {
this.subsinkService.getString()
.subscribe((message: string) => {
this.message = message;
console.log(`${this.name} - ${message}`);
});
}
import { SubsinkService } from './../../services/subsink.service';
A name and message variables are defined. The subsink service is injected into the contructor of the class. In the ngOnInit method, we subscribe to the getString
method and then console log the values. The message and name properties are used in the card template file.
<div class="col">
<div class="card text-center bg-primary text-white" style="width: 20rem;">
<div class="card-header">{{name}}</div>
<div class="card-body">
<h5 class="card-title">{{message}}</h5>
</div>
</div>
</div>
You can see above that when the user types in a value and then click on the capitalize button, it displays the text inside the card component and also log some values in the browser's console. This is the normal behaviour to expect because the component is active in the DOM. When the close card button is clicked and the card is destroyed, you can see that the component still subscribes to the RxJS Observable when the capitalize button is clicked. This is not a behaviour that we want and it is as a result of not unsubscribing from the Observable. If this behaviour continues, it will lead to memory leak.
One of the many solutions to use in fixing the behaviour is by using the SubSink module (npm). This module absorbs the RxJS subscriptions in an array and when the unsubscribe method is called, it gracefully unsubscribes all added subscriptions.
First, install the subsink module
npm i subsink
Add this to the subsink.component.ts file and import the subsink module and service file
name = 'Subsink component 1 - Test';
message: string;
private subs = new SubSink();
constructor(private subsinkService: SubsinkService) { }
ngOnInit() {
this.subs.sink = this.subsinkService.getString()
.subscribe((message: string) => {
this.message = message;
console.log(`${this.name} - ${message}`);
});
}
ngOnDestroy() {
this.subs.unsubscribe();
}
import { SubSink } from 'subsink';
import { SubsinkService } from 'src/app/services/subsink.service';
Just like how we did with the card component, here we added the extra SubSink property. An instance of the SubSink class is created. Then, the getString
method's subscription is set equal to the sink property from the SubSink class.
this.subs.sink = this.subsinkService.getString()
.subscribe((message: string) => {
this.message = message;
console.log(`${this.name} - ${message}`);
});
The sink
property is used to assign subscription so as to add it to the tracked subscriptions. The subscription is removed inside the ngOnDestroy method. That is, when the component is destroyed. Add the template
<div class="col">
<div class="card text-center bg-success text-white" style="width: 20rem;">
<div class="card-header">{{name}}</div>
<div class="card-body">
<h5 class="card-title">{{message}}</h5>
</div>
</div>
</div>
You can see from the above image that when both components are active in the DOM, they subscribe to the Observable but when both components are destroyed, the subscription for the subsink component stops while the other card component still subscribes. In the code above there is only one subscription. There might be cases where more than one subscription is needed in the same class. SubSink provides a method than can be used to add all subscriptions to the array and automatically unsubscribe from all at once.
Add this code to the template file
<div class="col">
<div class="card text-center bg-warning text-white" style="width: 20rem;">
<div class="card-header">{{name2}}</div>
<div class="card-body">
<h5 class="card-title">{{msg}}</h5>
</div>
</div>
</div>
Also, to the subsink component class, add
name2 = 'Subsink component 1 - Test';
msg: string;
subSinkSubscriptions() {
this.subs.add(
this.subsinkService.getString()
.subscribe((message: string) => {
this.message = message;
console.log(`${this.name} - ${message}`);
}),
this.subsinkService.getString()
.subscribe((message: string) => {
this.msg = message;
console.log(`${this.name2} - ${message}`);
})
);
}
Comment the code inside the ngOnInit method and add this new method
this.subSinkSubscriptions();
In the above method, the add
method from subsink is used to add the subscriptions to the tracked subscriptions. With this add
method, you can add multiple subscriptions to the array and unsubscribe from them all when the component is destroyed.
Conclusion
When a component is destroyed, all custom Observables need to be unsubscribed manually. The SubSink approach is not the only available approach that can be used to unsubscribe from RxJS Observables. There are operators from RxJS like takeWhile
and takeUntil
that can be used. Also, AsyncPipe can be used to unsubscribe from an Observable as well as the unsubscribe method from RxJS Subscription
class.
Find the github repo for the sample app here.
If you prefer watching a video tutorial for this article, you can check it our here
If you are interested in more Angular related stuff, you can follow me Twitter and also subscribe to my YouTube channel.
Posted on June 8, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.