New Best Practice
Sign In

Unsubscribe from RxJS Observables to avoid memory leak

Wednesday, October 23, 2019

RxJS is the incredible powerful reactive extensions for JavaScript library, but as Peter Parker's uncle Ben says: "With great power comes great responsibility".

Our responsibility when subscribing to an observable stream is to be sure that we unsubscribe. We unsubscribe when we not longer need additional (next, complete or error) notifications. In a component based framework such as React or Angular it is most common to unsubscribe from a subscription when the component is unmounted or destroyed. Both React and Angular provide a mechanism, or life-cycle method, for cleaning things up when a component is unmounted or destroyed.

If we fail to unsubscribe we could have a possible memory leak in our application. The memory leak is caused by unnecessary subscriptions.

It is important to understand that the signature for the subscribe() method in the Observable class is:

subscribe(observerOrNext?: PartialObserver<T> | ((value: T) => void), error?: (error: any) => void, complete?: () => void): Subscription

There is a lot of type declarations in the above signature. For now, let's focus on the return type: Subscription. This informs us that when we invoke the subscribe() method that we receive a new Subscription instance.

The Subscription class enables us to manage a subscription. Let's have a quick look at the Subscription class:

class Subscription implements SubscriptionLike {
  static EMPTY: Subscription
  constructor(unsubscribe?: () => void)
  closed: [object Object]
  unsubscribe(): void
  add(teardown: TeardownLogic): Subscription
  remove(subscription: Subscription): void
}

Take note of the following methods:

  • unsubscribe() will unsubscribe this subscription, along with all child subscriptions
  • add() will add a child subscription to this subscription

Using these methods we can unsubscribe from RxJS observables to avoid a memory leak.

Code Examples

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { Rule } from '@lkt-core/models';
import { ConfirmationDialogComponent, ConfirmationDialogData } from '@lkt-shared/components';
import { RuleFacade } from '@lkt-state/rule';
import { Observable } from 'rxjs';

@Component({
  templateUrl: './index.component.html',
  styleUrls: ['./index.component.scss']
})
export class IndexComponent implements OnInit {
  /** Toggle loading indicator. */
  loading: Observable<boolean>;

  /** The rules to display. */
  rules: Observable<Array<Rule>>;

  constructor(private readonly matDialog: MatDialog, private readonly ruleFacade: RuleFacade) {}

  ngOnInit() {
    this.loading = this.ruleFacade.loading;
    this.rules = this.ruleFacade.loadAll();
  }

  onRemove(rule: Rule): void {
    const data: ConfirmationDialogData = { title: 'Remove Rule' };
    const dialogRef = this.matDialog.open(ConfirmationDialogComponent, {
      data
    });
    dialogRef.afterClosed().subscribe(value => value && this.ruleFacade.remove(rule));
  }
}

do: Foo

Instructions

  • Do:

    Use Subscription.add() to add child subscriptions

  • Do:

    Invoke Subscription.unsubscribe(), often when unmounting or destroying a component

  • Avoid:

    The takeUntil() operator approach.

  • Why:

    Similar to event listeners, open subscriptions that are unecessary leads to memory leaks in your application

Brian Love

Brian is a software engineer and Google Developer Expert in Web Technologies and Angular with a passion for learning, writing, speaking, teaching and mentoring. He regularly speaks at conferences and meetups around the country, and co-authored "Why Angular for the Enterprise" for O'Reilly. When not coding, Brian enjoys skiing, hiking, and being in the outdoors. Brian recently launched lookout.dev where you can find best practices and expert advice on topics ranging from TypeScript, Angular, React, Node.js and more.

Google Developers Expert

Discussions are healthy ❤️