When using RxJS, we must make sure that we handle the lifecycle of our subscriptions properly. Otherwise, we risk introducing memory leaks and lifecycle related defects into our application. In the sections below, we'll outline and analyse some common patterns for observable subscription management. If you would like to learn more about the core concepts of RxJS before proceeding, please visit our Introduction to Observables with RxJS.
Observables vs Subscriptions
Before proceeding into the main content, we'll provide a brief description of observables and subscriptions to outline the differences between them:
- Observable: an object which pushes / emits zero or more values.
- Subscription: an object which is created when an observable is subscribed to and provides an unsubscribe method to dispose of the subscription.
If and when an observable completes, any subscription to it will automatically be unsubscribed. In many cases, an observable will never emit a completion event.
What Happens When You Don't Unsubscribe?
If a subscription is never unsubscribed from, and the observable it subscribed to never completes, a memory leak will occur, even if the place in which the subscription was created itself is no longer in memory. For example: if we have a web component which initiates a subscription, then is dereferenced, the subscription may still be active and invoked when the observable emits values. This will cause a memory leak, and if multiple observables are handled in this way, multiple memory leaks will occur. Even a single memory leak can cause huge performance issues depending upon what happens in the subscription callback.
Note that when an observable emits a complete event, subscriptions will automatically be unsubscribed, and that certain frameworks using RxJS may also handle unsubscriptions automatically in certain cases. In all other cases, we must handle unsubscribing ourselves.
1. Storing Individual Subscriptions
The simplest way to handle a subscription is to store the subscription in a variable for later use. When subscribe is called, it will return an object which we can call unsubscribe from when we're ready:
In most contexts, we will be using S within a framework, such as Angular, or if not, within a modern JavaScript context. In those cases, observables will be used within a context of a component's lifecycle which will have methods such as onInit and onDestroy. In that context, the subscription lifecycle will look something like:
2. Storing a List of Subscriptions
In many situations, more than one subscription will be needed, possibly a dynamic / variable amount of them. A common practice is to use an array of Subscription objects, which is initially empty, add subscriptions as we need to, then when a certain point is reached, such as onDestroy, iterate over the array and unsubscribe from all subscriptions:
This provides the benefit of being able to store any number of subscriptions within a single variable, as opposed to creating a new variable for each one. If there is a situation where subscriptions need to be unsubscribed at different times, this approach will need to be adapted to have more than one array or variable, one for each unsubscribe point.
3. Take 1 or More
In some situations, we only need to take data from the subscription the first time it emits; subsequent emissions will be irrelevant. A few common use cases include:
- Initializing a component based on some data in existing observables.
- Taking an action when a certain type of event occurs for the first time, such as error logging, without reacting to subsequent emissions.
- Obtaining a value which is needed *as if* it were synchronous.
We can implement this type of subscription by using the take pipe. The pipe accepts a number parameter which indicates how many emit events to take before completing. To take 1, we pass 1 in as the argument. For example:
Notice in the code above we do not store the subscription or call unsubscribe directly. The take pipe will handle the unsubscription for us by completing the observable itself after the input number of events are emitted. Therefore, calling unsubscribe is not strictly necessary to prevent the normal memory leak issues. However: the observable will still need to emit before the completion occurs, so less-severe memory leaks can still possibly occur with this method if we do not unsubscribe ourselves.
To ensure memory leaks are prevented entirely, we can still keep track of the subscription and unsubscribe as previously described. If we are certain the event will emit immediately or before the lifecycle of the component / class / etc completes, we can omit the unsubscription for brevity.
4. First Value From
Using the firstValueFrom pipe, we can combine observables with the async / await keywords used with promises. This gives us a way of obtaining the current / next value of an observable using promise-like syntax:
Similarly, lastValueFrom can be used to get the very last value emitted before the observable completes. Note: if you await this call and the observable never completes, the execution of this function will be stalled.
5. Unsubscribing via TakeWhile Pipe
In some use cases, we may want to do something which resembles unsubscribing within a subscrption itself depending upon a set of conditions. For example:
However, the code above is problematic, as we are referencing sub before the value has been assigned. Fortunately, we can use the pipe takeWhile to achieve the intended end result:
Each time the observable emits a value, the callback inside takeWhile will be invoked:
- If the returned value is true: emit the event and invoke the subscription.
- If the value is false: complete the observable and do not invoke the subscription.
Similar to the take situation, if we want to guarantee that no memory leaks occur, we can still track the subscription and unsubscribe when appropriate:
If we call unsubscribe, the subscription callback will no longer be invoked, even if the *takeWhile* pipe has never returned false.
6. Controlling Completion with Subjects
An alternative to tracking subscriptions with variables or arrays is to use a subject to control observable completion. In this method we will:
- Create a subject unsubscribeSubject$ which can be referenced wherever we need to subscribe to an observable.
- In each subscription, add a pipe takeUntil(unsubscribeSubject$) before subscribing.
- When it is time to unsubscribe, emit an event and complete the unsubscribeSubject$.
When the takeUntil pipe is used, the piped observable will automatically complete when the observable passed into it emits a value. This method is equally as effective as the method of storing subscription references and unsubscribing manually. This also provides the advantage that the completion is reactive: we can pass an observable between multiple components / services and have them all complete their observables when the event is emitted.
Note: in order to ensure that takeUntil works as intended, ensure it is the last pipe in the observable's pipes if there are more than one pipes. This will ensure that subsequent pipes do not themselves forestall completing the observable before it reaches the subscription.
Avoid Immediate Subscribe & Unsubscribe
One common practice is to immediately invoke unsubscribe after invoking subscribe:
However, we recommend that this practice be avoided. This assumes the observable will emit the event synchronously. If this is not the case, or if the code is refactored in the future to not emit synchronously, the callback will never be triggered. An alternative way to achieve the intended result is described in the Take 1 to Promise section below.