Tackling Angular Subscriptions with RxJS

Welcome to another session where we discuss pretty interesting facets of Angular. In this post, we are going to explore some common patterns and pitfalls related to unsubscription from RxJS streams, a scenario that arises when a component, directive, or anything else is getting destroyed.

This blog post is based on a YouTube video by Dmytro Mezhenskyi that discusses various ways to handle Angular subscriptions using RxJS. If you want to follow along, you can find the video below: 

Here is a Stackblitz example of the code we will elaborate about: stackblitz.com/edit/stackblitz-starters-agurve

Using takeUntil Operator

One of the popular patterns involves the takeUntil operator combined with an RxJS subject. The concept is fairly straightforward. When a component is destroyed, the subject emits a value which prompts the takeUntil operator to complete the stream.

destroyed$ = new Subject();
$data = interval(1000).pipe(takeUntil(this.destroyed$));

ngOnDestroy() {
  this.destroyed$.next();
  this.destroyed$.complete();
}

However, things might not always go as planned. An innocent piece of code may result in a memory leak if not handled properly. This can happen even when we use the takeUntil operator. You might wonder, why is this happening? Well, the problem lies in the order of the operators. 

To achieve the expected behavior and prevent a memory leak, it is important to move the takeUntil operator below the long-living nested subscription operator. i.e. switchMap. Placing takeUntil above switchMap will result in a continuous emission of values without stopping. It’s crucial to remember this fundamental characteristic of operators like takeUntil when working with stream-completing operators such as take, takeWhile, first and others.

For instance, when an operator completes a stream, it initiates a tear-down logic in the operators positioned above it in the chain. This tear-down logic performs a clean-up, such as unsubscribing from the inner observable in the case of the switchMap operator.

Check the whole code example on stackblitz 👍

takeUntilDestroy Operator and Other Operators

What about the brand-new operator introduced with Angular 16 called takeUntilDestroy? It turns out that takeUntilDestroy doesn’t solve this issue either. This operator follows the same pattern (takeUntil with Subject) and utilizes the takeUntil operator under the hood. So, if you use this operator, you need to make sure to use it in the proper order as well.

On the whole, when handling unsubscriptions in your Angular application, it’s generally recommended to place takeUntil with subject or takeUntilDestroy operator at the end of the operator chain. However, like with everything in RxJS, exceptions exist.

Understanding the Exceptions

Some operators emit values just before the stream is completed. Examples of these include last, takeLast, toArray and others. If you use one of these operators, they need to be placed even after the takeUntil with subject or takeUntilDestroy operator, otherwise they won’t work as expected.

On a related note, it is essential to draw attention to a special eslint rule that is designed to detect and report incorrectly used takeUntil operators. This rule is instrumental in ensuring that these operators function correctly. This rule provides valuable assistance in mitigating issues related to the misuse of the takeUntil operator. It is a resource worth exploring to ensure your code follows best practices and maintains functionality integrity.

Traditional unsubscribe and async pipe

The traditional way to handle unsubscription is to use unsubscribe inside the ngOnDestroy lifecycle hook. While it works without pitfalls, you would need to do it for every subscription.

sub = new Subscription();

ngOnInit() {
   this.sub.add(this.polling.url$.subscribe(...));
}

ngOnDestroy() {
   this.sub.unsubscribe();
}

Alternatively, managing unsubscribing using the async pipe can be a clean and safe approach as it automatically unsubscribes or kills the subscription when the corresponding view is destroyed. However, it has a downside too. The async pipe is available only in templates, and not in Angular services or directives.

If you would like to find out more about how an async pipe works – you can jump into my previous article about that.

A Better Way to Handle Unsubscriptions

The question then arises, is there a better way? A way that takes the best from the async pipe and can be used across services or directives? 

Yes, there is! The solution lies in the new RxJS interop API, which is currently in Angular developer preview (Angular 16). This API allows you to convert an observable to a signal using a helper function called toSignal. The resulting signal can be used everywhere, just like in services or Angular directives. The toSignal function automatically handles unsubscription from the RxJS stream when the corresponding scope, like component, directive, service, etc., is destroyed.

Nevertheless, while the toSignal function is a powerful tool, it is important to consider its potential disadvantages and unique behavior. Primarily, since toSignal uses inject() under the hood, its use is limited to the constructor or Injector context. This could pose limitations in certain scenarios depending on the structure of your application and your specific use cases.

Moreover, toSignal subscribes to the observable instantly regardless of whether the resulting signal is used or not. This behavior contrasts with how the async pipe operates. While not inherently a disadvantage, it is an important consideration that could affect the performance and behavior of your application. Understanding this characteristic is key to effectively utilizing the toSignal function and ensuring that your code behaves as expected.

Despite these points of caution, the new RxJS interop API provides a robust and efficient way to manage unsubscriptions, given that these nuances are appropriately accounted for.

import { toSignal } from '@angular/core/rxjs-interop';
// ...
dataSignal = toSignal(this.polling.url$.pipe(...));

Wrapping Up

That’s it for now. I hope this post has shed light on some effective ways of handling unsubscriptions in Angular applications using RxJS. 

Remember, understanding the correct usage and order of RxJS operators is crucial in preventing memory leaks and ensuring smooth operation of your Angular applications.

Have a productive week ahead, stay safe, and see you in the next post!

About the author

Michał Grzegorczyk

Angular Developer

Add Comment

Tags

Michał Grzegorczyk

Angular Developer

Get in touch

Decoded Frontend
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.