Managing RxJS Subscriptions using Angular

There are many how-to’s on the web describing how to manage RxJS subscriptions in Angular apps. In this article, I’m going to share with you how I handle it myself. And why I do believe it’s one of the cleanest ways possible.

This is image title

Oftentimes, you deal with multiple RxJS observables inside one of your Angular components. You need to subscribe to them somewhere and more importantly unsubscribe from them when they are no longer needed.

There are thousands of articles on the web explaining how important it is to always unsubscribe. The memory leakage, the performance drop, crazy side-effects — these all can be triggered by a single not-unsubscribed observable.

OK, so we know we must unsubscribe. Now we need to know how…

There are again thousands of other articles explaining how to unsubscribe properly. How not to overload your component code with the code managing the subscriptions.

You can use RxJS operators to help yourself, or you can use the ngOnDestroy hook, or a Subject, or a component base class.

There are many ways apparently. But all of them, no matter how much they try not to, make your component code a bit messy. Your application logic coded in the component is then mixed with some subscription-managing code you have added to prevent that dreadful memory leakage.

It definitely does not work well for readability. Even very simple components start to look a bit complex and scary…

Async Pipe to the Rescue?

OK. But come on, I just want to subscribe to some observables when the component is created and then unsubscribe from all when it’s destroyed.

Why do I have to write a single line of code for that? It seems to be the most common case. Why do I have to handle it on my own? Can’t the framework help me with that?

Well, it can. There’s the async pipe in Angular.

You can use the async pipe in your component template. You pass to it an observable and then it subscribes to it, it returns values from it and finally it unsubscribes from it when the component is destroyed. Everything we need. At least, at first glance…

Imagine we have a lastComment$ observable returning the last comment object. And, imagine we want to print the comment body in the template. We can do it easily like so:

<h1>Last comment</h1>
<div>{{ (lastComment$ | async).body }}</div>

So far so good. But what if we want to add the comment subject also?

<h1>Last comment</h1>
<h2>{{ (lastComment$ | async).subject }}</h2>
<div>{{ (lastComment$ | async).body }}</div>

Uhm… not so cool anymore. We have two subscriptions instead of one. Each use of the async pipe creates a separate subscription (even if you use the same observable).

Imagine that under the lastComment$ observable, you have hidden an HTTP request to an API or some complex data processing. With two separate subscriptions, you will do those operations twice instead of once. It does not sound OK, does it? So, what can we do?

Well, we can help ourselves by making use of the *ngIf directive. Let’s wrap everything in a div element. Let’s add *ngIf to it and subscribe to lastComment$ inside of it.

<div *ngIf="lastComment$ | async">
...
</div>

Whenever there will be a comment emitted, the div content will be shown. Whenever there is no comment at all, the entire div will be hidden. Perfect.

We can keep the async pipe result under the variable and use it later in the element revealed by the *ngIf. It’s something you’ve probably seen many times. Let’s call our variable comment.

<div *ngIf="(lastComment$ | async) as comment">
  <h1>Last comment</h1>
  <h2>{{ comment.subject }}</h2>
  <div>{{ comment.body }}</div>
</div>

OK, so it looks like we nailed it, right?

Yeah… But…

Now imagine your component template is a bit bigger. And you display the comment data in two or more places, separated by some other content. Let’s say we want to print the comment author’s name, then a blog post, and then all the comment details, like so:

<div *ngIf="(lastComment$ | async) as comment">
  This was recently commented by {{ comment.author }}!
</div>
<div>
  <h1>Post</h1>
  <h2>{{ blogPost.subject }}</h2>
  <div>{{ blogPost.body }}</div>
</div>
<div *ngIf="(lastComment$ | async) as comment">
  <h1>Last comment</h1>
  <h2>{{ comment.subject }}</h2>
  <div>{{ comment.body }}</div>
</div>

Uhm… Again. As you can see, we have multiple subscriptions.

Can we deal with them? Will our trick — the wrapping div — will it work again? Let’s see:

<div *ngIf="(lastComment$ | async) as comment">
  <div>
    This was recently commented by {{ comment.author }}
  </div>
  <div>
    <h1>Post</h1>
    <h2>{{ blogPost.subject }}</h2>
    <div>{{ blogPost.body }}</div>
  </div>
  <h1>Last comment</h1>
  <h2>{{ comment.subject }}</h2>
  <div>{{ comment.body }}</div>
</div>

And, no…It doesn’t work anymore — at least not always.

Please notice: when there is no lastComment at all, the async pipe returns null. And then the *ngIf directive hides the entire div with all its content, together with the innocent blogPost element that we want to keep visible.

So, is the async pipe useless in such scenarios? Are we forced to manage subscriptions manually? Or do we need to live with multiple subscriptions in templates?

Luckily, you can still use the async pipe!You just need to know how.

Asyncing Like a Pro

We just need to create a new observable from the lastComment$ observable and make sure it always returns a truthy value. Let’s transform our lastComment$ observable a little bit, and let’s call it componentContext$.

let componentContext$ = this.lastComment$.pipe(
  map(lastComment => ({ lastComment }))
);

And, done! Please notice that our new observable always returns a truthy value (object), even when lastComment$ emits null. In case of null, the returned value will look like follows: { lastComment: null }.

Let’s see it in action now:

<div *ngIf="(componentContext$ | async) as ctx">
  <div *ngIf="ctx.lastComment as comment">
    This was recently commented by {{ comment.author }}
  </div>
  <div>
    <h1>Post</h1>
    <h2>{{ blogPost.subject }}</h2>
    <div>{{ blogPost.body }}</div>
  </div>
  <div *ngIf="ctx.lastComment as comment">
    <h1>Last comment</h1>
    <h2>{{ comment.subject }}</h2>
    <div>{{ comment.body }}</div>
  </div>
</div>

Everything works. The wrapping div is always visible, as the componentContext$ emits values that are always truthy. Inner divs are shown or hidden depending on the lastComment value.

We inspect the ctx.lastComment property in the corresponding inner *ngIfs and react appropriately.

See how clean it looks. We have one async pipe, one subscription, and a fully working component.

Above all, all the subscriptions are now controlled by the async pipe itself, and we do not have to worry about the timely unsubscription, as Angular does it for us.

Component Context Observable

The observable we have created above — I like to call it a component context observable.

It’s a single observable composed of all observables your component is using. It emits the component context object containing all the data the component needs to properly render.

To make sure our context observable emits every time, once one of its source observables produces a value, it’s handy to create it using the combineLatest operator.

In general, this could be created like so:

@Component({...})
export class ExampleComponent implements OnInit {
ngOnInit() {
  ...
  this.context$ = combineLatest(
    this.blogPost$, 
    this.lastComment$
  ).pipe(
    map(([blogPost, lastComment]) => {
      return { blogPost, lastComment };
    })
  );
  ...
}
}

Once configured, it can be used like so:

<ng-container *ngIf="(context$ | async) as ctx">
<!-- component body below -->
  <div *ngIf="ctx.lastComment as comment">
    This was recently commented by {{ comment.author }}
  </div>
  <div *ngIf="ctx.blogPost as blogPost">
    <h1>Post</h1>
    <h2>{{ blogPost.subject }}</h2>
    <div>{{ blogPost.body }}</div>
  </div>
<!-- component body above -->
</ng-container>

See how this approach can structure the way you manage your RxJS observables in components. We could summarize this in the following three rules which you can stick to across your entire application:

  1. Never subscribe to observables directly in the component code. You will never need to manage their subscriptions if you don’t. Use async pipewhenever you need to subscribe to an observable in the component.
  2. Create the component context observable which is composed of the observables you want to use in your component. Make sure it always emits a truthy value.
  3. Subscribe to the context observable right at the beginning of your component template using the async pipe and *ngIf and pass the emitted context object to the rest of the template via the variable. All the subscriptions will be then be terminated by the async pipe once the component gets destroyed.

Happy coding!

#angular #RxJS #javascript #programming

Managing RxJS Subscriptions using Angular
11.25 GEEK