loading...

Unsubscribe from RxJS subscriptions using SubSink

uzochukwueddie profile image Uzochukwu Eddie Odozi Updated on ・6 min read

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 by:

Discussion

pic
Editor guide