
Let's dive into some engaging aspects of Angular. This article examines frequent patterns and traps related to unsubscribing from RxJS streams—a common situation when a component, directive, or other entity is being torn down.
This content draws from a YouTube presentation by Dmytro Mezhenskyi covering different techniques for managing Angular subscriptions with RxJS. The referenced video is embedded below for your convenience:
A Stackblitz project illustrating the code we'll discuss is available here: stackblitz.com/edit/stackblitz-starters-agurve
A widely adopted strategy involves pairing the takeUntil operator with an RxJS subject. The principle is quite simple: upon component destruction, the subject issues a value, causing the takeUntil operator to finalize the stream.
destroyed$ = new Subject();
$data = interval(1000).pipe(takeUntil(this.destroyed$));
ngOnDestroy() {
this.destroyed$.next();
this.destroyed$.complete();
}
Yet, things aren't always straightforward. A seemingly harmless snippet can introduce a memory leak if not carefully crafted. This holds true even when the takeUntil operator is used. You might ask why. The culprit is often the sequence of operators in the chain.
To achieve the desired outcome and avert a memory leak, you should position the takeUntil operator after the long-running nested subscription operator, such as switchMap. If takeUntil sits above switchMap, values will keep flowing endlessly without termination. This fundamental behavior of operators like takeUntil, as well as stream-terminating ones like take, takeWhile, first, and others, is essential to keep in mind.
For instance, when an operator finalizes a stream, it triggers teardown logic in the operators located above it in the chain. This teardown logic handles cleanup, like unsubscribing from the inner observable in the case of the switchMap operator.


Feel free to inspect the complete code example on stackblitz 👍
Now, what about the new operator introduced with Angular 16, known as takeUntilDestroy? It appears that takeUntilDestroy doesn't resolve this issue either. This operator adheres to the same pattern (takeUntil with Subject) and employs the takeUntil operator internally. Thus, when using this operator, you must also pay attention to the correct order in the chain.
Generally speaking, for unsubscription handling in Angular, it's advisable to position takeUntil with a subject, or the takeUntilDestroy operator, at the point where the source observable is transformed or combined. However, as with most things in RxJS, there are exceptions to this guideline.
Certain operators emit their final values just before the stream completes. Examples include last, takeLast, toArray, and similar ones. If you employ these operators, they must appear in the chain after the takeUntil with subject, or the takeUntilDestroy operator; otherwise, they won't behave as expected.
In a related vein, it's worth highlighting a specialized eslint rule that flags incorrect usage of the takeUntil operator. This rule is quite useful for verifying that these operators are used correctly. It offers significant help in minimizing problems stemming from the misuse of the takeUntil operator. It's a resource worth checking out to guarantee that your code adheres to best practices and maintains its intended functionality.
The conventional method for managing unsubscription involves invoking unsubscribe within the ngOnDestroy lifecycle hook. While this approach works without major pitfalls, it requires you to handle unsubscription for every single subscription you create.
sub = new Subscription();
ngOnInit() {
this.sub.add(this.polling.url$.subscribe(...));
}
ngOnDestroy() {
this.sub.unsubscribe();
}
On the other hand, using the async pipe for unsubscription management can be a clean and safe option, as it automatically unsubscribes, or effectively cancels, the subscription when the associated view is destroyed. However, it does have a limitation: the async pipe can only be used within templates, not in Angular services or directives.
For a deeper dive into how async pipes operate, you can refer to my previous article on the topic.
The question then arises: is there an improved method? One that combines the strengths of the async pipe but works across services or directives?
Indeed, there is! The answer rests in the new RxJS interop API, which is in Angular developer preview (Angular 16). This API enables you to transform an observable into a signal via a helper function called toSignal. The resulting signal can be used in various contexts, similar to services or Angular directives. The toSignal function automatically manages unsubscription from the RxJS stream when the relevant scope, such as a component, directive, service, etc., is destroyed.
However, while the toSignal function is indeed potent, it's important to weigh its potential drawbacks and unique characteristics. Notably, since toSignal relies on inject() under the hood, its usage is restricted to the constructor or Injector context. This can pose limitations in certain scenarios, depending on your application's architecture and your specific requirements.
Furthermore, toSignal subscribes to the observable immediately, regardless of whether the resulting signal is ever consumed. This behavior contrasts with that of the async pipe. Though not inherently a drawback, it's an important factor to consider that could influence your application's performance and behavior. Grasping this nuance is vital for effectively wielding the toSignal function and ensuring your code performs as intended.
Despite these cautionary notes, the new RxJS interop API offers a solid and efficient method for handling unsubscriptions, provided that these subtle aspects are properly addressed.
import { toSignal } from '@angular/core/rxjs-interop';
// ...
dataSignal = toSignal(this.polling.url$.pipe(...));
That brings us to the end of this discussion. I trust that this article has shed light on some practical methods for managing unsubscriptions in Angular applications using RxJS.
Always keep in mind that mastering the proper use and ordering of RxJS operators is essential for avoiding memory leaks and ensuring your Angular applications run smoothly.
Wishing you a productive week ahead, stay safe, and I'll see you in the next article!
