DEV Community

Cover image for Best Practices for Managing RxJS Subscriptions
Colum Ferry for This Dot

Posted on • Originally published at labs.thisdot.co

Best Practices for Managing RxJS Subscriptions

When we use RxJS, it's standard practice to subscribe to Observables. By doing so, we create a Subscription. This object provides us with some methods that will aid in managing these subscriptions. This is very important, and is something that should not be overlooked!

Why do we care about subscription management?

If we do not put some thought into how we manage and clean up the subscriptions we create, we can cause an array of problems in our applications. This is due to how the Observer Pattern is implemented.

When an Observable emits a new value, its Observers execute code that was set up during the subscription. For example:

obs$.subscribe(data => doSomethingWithDataReceived(data));
Enter fullscreen mode Exit fullscreen mode

If we do not manage this subscription, every time obs$ emits a new value doSomethingWithDataReceived will be called.

Let's say this code is set up on the Home View of our App. It should only ever be run when the user is on the Home View. Without managing this subscription correctly when the user navigates to a new view in the App, doSomethingWithDataReceived could still be called, potentially causing unexpected results, errors or even hard-to-track bugs.

So what do we mean by Subscription Management?

Essentially, subscription management revolves around knowing when to complete or unsubscribe from an Observable, to prevent incorrect code from being executed, especially when we would not expect it to be executed.

We can refer to this management of subscriptions as cleaning up active subscriptions.

How can we clean up subscriptions?

So, now that we know that managing subscriptions are an essential part of working with RxJS, what methods are available for us to manage them?

Unsubscribing Manually

One method we can use is to unsubscribe manually from active subscriptions when we no longer require them. RxJS provides us with a convenient method to do this. It lives on the Subscription object and is called .unsubscribe().

If we take the example we had above; we can see how easy it is to unsubscribe when we need to:

let homeViewSubscription = null;

function onEnterView() {
  homeViewSubscription = obs$.subscribe(data => doSomethingWithDataReceived(data));
}

function onLeaveView() {
  homeViewSubscription.unsubscribe();
}
Enter fullscreen mode Exit fullscreen mode
  1. We create a variable to store the subscription.
  2. We store the subscription in a variable when we enter the view.
  3. We unsubscribe from the subscription when we leave the view preventing doSomethingWithDataReceived() from being executed when we don't need it.

This is great; however, when working with RxJS, you will likely have more than one subscription. Calling unsubscribe for each of them could get tedious. A solution I have seen many codebases employ is to store an array of active subscriptions, loop through this array, unsubscribing from each when required.

Let's modify the example above to see how we could do this:

const homeViewSubscriptions = [];

function onEnterView() {
  homeViewSubscriptions.push(
      obs$.subscribe(data => doSomethingWithDataReceived(data)),
      anotherObs$.subscribe(user => updateUserData(user))
    );
}

function onLeaveView() {
  homeViewSubscriptions.forEach(subscription => subscription.unsubscribe());
}
Enter fullscreen mode Exit fullscreen mode
  1. We create an array to store the subscriptions.
  2. We add each subscription to the array when we enter the view.
  3. We loop through and unsubscribe from the subscriptions in the array.

These are both valid methods of managing subscriptions and can and should be employed when necessary. There are other options. However, that can add a bit more resilience to your management of subscriptions.

Using Operators

RxJS provides us with some operators that will clean up the subscription automatically when a condition is met, meaning we do not need to worry about setting up a variable to track our subscriptions.

Let's take a look at some of these!

first

The first operator will take only the first value emitted, or the first value that meets the specified criteria. Then it will complete, meaning we do not have to worry about manually unsubscribing. Let's see how we would use this with our example above:

function onEnterView() {
    obs$.pipe(first())
        .subscribe(data => doSomethingWithDataReceived(data))
}
Enter fullscreen mode Exit fullscreen mode

When obs$ emits a value, first() will pass the value to doSomethingWithDataReceived and then unsubscribe!

take

The take operator allows us to specify how many values we want to receive from the Observable before we unsubscribe. This means that when we receive the specified number of values, take will automatically unsubscribe!

function onEnterView() {
    obs$.pipe(take(5))
        .subscribe(data => doSomethingWithDataReceived(data))
}
Enter fullscreen mode Exit fullscreen mode

Once obs$ has emitted five values, take will unsubscribe automatically!

takeUntil

The takeUntil operator provides us with an option to continue to receive values from an Observable until a different, notifier Observable emits a new value.

Let's see it in action:


const notifier$ = new Subject();

function onEnterView() {
      obs$.pipe(takeUntil(notifier$)).subscribe(data => doSomethingWithDataReceived(data))
}

function onLeaveView() {
  notifier$.next();
  notifier$.complete();
}

Enter fullscreen mode Exit fullscreen mode
  1. We create a notifier$ Observable using a Subject. (You can learn more about Creating Observables here.)
  2. We use takeUntil to state that we want to receive values until notifier$ emits a value
  3. We tell notifier$ to emit a value and complete _(we need to clean notifer$ up ourselves) when we leave the view, allowing our original subscription to be unsubscribed.

takeWhile

Another option is the takeWhile operator. It allows us to continue receiving values whilst a specified condition remains true. Once it becomes false, it will unsubscribe automatically.

function onEnterView() {
      obs$
        .pipe(takeWhile(data => data.finished === false))
        .subscribe(data => doSomethingWithDataReceived(data))
}
Enter fullscreen mode Exit fullscreen mode

In the example above we can see that whilst the property finished on the data emitted is false we will continue to receive values. When it turns to true, takeWhile will unsubscribe!

BONUS: With Angular

RxJS and Angular go hand-in-hand, even if the Angular team has tried to make the framework as agnostic as possible. From this, we usually find ourselves having to manage subscriptions in some manner.

async Pipe

Angular itself provides one option for us to manage subscriptions, the async pipe. This pipe will subscribe to an Observable in the template, and when the template is destroyed, it will unsubscribe from the Observable automatically. It's very simple to use:

<div *ngIf="obs$ | async as data">
  {{ data | json }}
</div>
Enter fullscreen mode Exit fullscreen mode

By using the as data, we set the value emitted from the Observable to a template variable called data, allowing us to use it elsewhere in the children nodes to the div node.

When the template is destroyed, Angular will handle the cleanup!

untilDestroyed

Another option comes from a third-party library developed by Netanel Basal. It's called until-destroyed, and it provides us with multiple options for cleaning up subscriptions in Angular when Angular destroys a Component.

We can use it similarly to takeUntil:

import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';

@UntilDestroy()
@Component({
    selector: 'home'
})
export class HomeComponent implements OnInit {
  ngOnInit() {
    obs$
      .pipe(untilDestroyed(this))
      .subscribe(data => this.doSoemthingWithDataReceived(data));
  }
}
Enter fullscreen mode Exit fullscreen mode

It can also find which properties in your component are Subscription objects and automatically unsubscribe from them:

@UntilDestroy({ checkProperties: true })
@Component({
    selector: 'home'
})
export class HomeComponent {

  subscription = obs$
      .pipe(untilDestroyed(this))
      .subscribe(data => this.doSoemthingWithDataReceived(data));
}
Enter fullscreen mode Exit fullscreen mode

This little library can be beneficial for managing subscriptions for Angular!

When should we employ one of these methods?

The simple answer to this question would be:

When we no longer want to execute code when the Observable emits a new value

But that doesn't give an example use-case.

  • We have covered one example use case in this article: when you navigate away from a view in your SPA.
  • In Angular, you'd want to use it when you destroy Components.
  • Combined with State Management, you could use it only to select a slice of state once that you do not expect to change over the lifecycle of the application.
  • Generally, you'd want to do it when a condition is met. This condition could be anything from the first click a user makes to when a certain length of time has passed.

Next time you're working with RxJS and subscriptions, think about when you no longer want to receive values from an Observable, and ensure you have code that will allow this to happen!


This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit thisdotlabs.com.

This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit thisdot.co.

Top comments (1)

Collapse
 
monfernape profile image
Usman Khalil

That's actually quite a useful pattern. I remember unsubscribing manually throughout my code