Mastering Subscription Handling in Angular
At Halodoc, our journey with Angular has been filled with creating and enhancing various web applications. Drawing from this extensive experience, we're passionate about simplifying complex ideas and maximizing Angular's potential. In this article, we're diving into the world of subscription handling – a crucial topic that may seem challenging, but fear not! We'll guide you through it, offering techniques and insights to elevate your web development skills.
Understanding Observables and Subscriptions
Observable is an object that emits items over time, either synchronously or asynchronously. Observable can have many observers, so you can subscribe to already existing observable that already being used by another observer.
- Observable in Angular Docs
- Observable in RxJS Docs
An object called a subscription serves to record data on the status of an observer's subscription to an observable. A subscription includes callbacks for handling error and completion events as well as information on whether it has been unsubscribed or not. We need to call observable's subscribe()
function to get notification about the latest data from an observable.
Why We Need to Handle Subscription
The lack of subscription handling can cause many problems in your app, the most critical one is memory leak. Let's see this code snippet below:
From the example above we can see that we are subscribing to dataService.getListHospitals
with .subscribe()
function. Now, because we don't handle the subscription that we are using, whenever the user are moving to another page (e.g hospital-detail page) the Observable are still referencing to this component while it actually not needed at all. So basically, subscriptions remain active even after the components are destroyed, which means every subscription that still active even after the component is destroyed is still using unnecessary memory. Subscriptions are not bundled with the component; therefore, we also need to dispose of subscriptions when the components are being destroyed. Over time user will probably frequently go to this component and move away, and all of the subscription will accumulate over time without being properly cleaned up. Imagine if there are a lot of your components using subscription and not a single component are being handled properly. This will cause your application memory usage grow massively unnecessary and the consequences would be a low performance application or it might even crash.
Incomplete Data Processing
For example, you are subscribing to an observable and whenever you receives new data from the observable, you are modifying DOM based on the data you received. If the component is destroyed whilst your subscription is still active, it will still attempt to update the non-existent UI element. This will lead to memory leak, wasting resource unnecessarily because no one used it or view it. See below example:
Apps Unexpected Behavior
Without the proper handling of your subscription, your user might experience unexpected behavior. For instance, you were making http calls and you are not handling the subscription in case it encountered an error, based on your UI logic, it can be probably stuck on loading, broken UI, missing data and you have no idea what is the issue. For this issue, we need to handle the subscription if it encounters an error by using the error callback.
For error handling we can also utilize some RxJS Operators like retry
or catchError
More details here: https://rxjs.dev/api/operators/catchError
The Subscription Lifecycle
The subscription lifecycle basically can be divided into three parts, which is:
- Creation
Subscription is created when you are calling the subscribe()
function on an Observable, at this step the subscription is active and ready to listen for data emission from the Observable.
2. Data emission
When the subscription is active it is ready to listen for any data emission from the Observable, Subscription's associated callback function are triggered based on the type of data emitted by the Observable (next, error, complete).
3. Disposal
After you are done with the data emitted by the observable or the component that holds the subscription are destroyed, you should also destroy the subscription or it will cause problems like the one I mentioned earlier. To destroy the subscription we created we can use the unsubscribe()
method on the the ngOnDestroy()
function.
Ways to Handle Subscriptions in Angular
Unsubscribe in ngOnDestroy:
Previously we talk about the importance of unsubscribing subscription, then let's get down to the code.
The first step is to create a variable to contain the subscription, for this example we are creating subscription$
with the type Observable<any>
. After we are done with the variable, we can use the assign the subscribe function to the variable we have created. Now, we are going to use the feature of Angular Lifecycle OnDestroy, on the ngOnDestroy()
method, just call the variable we have created and then use the unsubscribe()
function. So, whenever the component is about to be destroyed, the method will be unsubscribe the current subscription.
Using the async Pipe:
Instead of manual subscriptions like the example above, we can alternatively use angular built-in features which is the async pipe.
On the example above we are using the Interval
function from RxJS to emit a new number every second, and then on the html template we are using the async pipe when printing the value. The built in async pipe means we are automatically subscribing to the value on initialization phase and will also automatically unsubscribe during the component destruction. With this, we can avoid the probability of forgetting to unsubscribe from the observable.
Composite Subscriptions:
Composite subscriptions are used when you want to manage multiple subscription as a single entity, so you can handle multiple subscription at once!. Instead of calling unsubscribe()
on every subscriptions we've made, we can just call one unsubscribe()
function on OnDestroy method.
Instead we can use this:
with Subscription.add()
function we can assign multiple subscriptions to a single variable, then we can simply call the unsubscribe()
function inside the ngOnDestroy()
method and all the subscription assigned by Subscription.add()
will be unsubscribed.
Take Until and Subject:
This time we are going to make us of rxjs-operators called the takeUntil
. This operator can be used when you want to stop a subscription within a certain condition, and that's why we also need to use Subject
to emit a value to satisfy the condition. Let's take a look of example code below:
Let's review what are we doing on above codes:
- We've created a new Subject called
$destroy
- On
ngOnInit
we are going to get the current params of the routes. To do so we need to subscribe to the angular built in ActivatedRoute. - When subscripting to ActivatedRoute, we are using takeUntil as pipe with the
$destroy
Subject. So, whenever the Subject emits a value the subscription will stop. - And then on
ngOnDestroy
we are callingthis.$destroy.next
to make the Subject emit a value and this will trigger thetakeUntil
operator to unsubscribe from the ActivatedRoute observable.this.$destroy.complete
are called to make sure the Subject are completed when the component are being destroyed.
takeUntilDestroyed in Angular 16
Angular 16 introduces a new operator to handle subscription and it's called takeUntilDestroyed
basically it serves as the same method like takeUntil
and subject except we can do it with less code and more flexibility!
Before talking about the new operator, angular 16 introduces another new flexible ngOnDestroy
that can be injected. So, basically you can get the destroyRef
from another component, which means you can set condition on your subscription based on another component destroyed status. Let's go into the code.
If you see at the above code, we are not using ngOnDestroy
at all, instead we are using the takeUntilDestroyed()
operator to take the subscription until the component are destroyed. Without passing any destroyRef
params into the operator, it will by default using the current context of destroyRef
. With this new operator our code are much cleaner and readable.
How about if we pass params to the operator?
Let's say we want to unsubscribe from the subscription until another component is destroyed. We can make use of the injectable destroyRef
to achieve this. See below code:
On the child component, we are injecting the destroyRef
so it can be accessed from the parent component. And then on the parent component instead of passing it blank on the takeUntilDestroyed
params, now we are actually passing the child component destroyRef
. This means whenever the child component is destroyed, the subscription on the parent component will also be unsubscribed.
Conclusion
As an Angular developer you would be working with a lot of Observable and Subscription, and it's important to know how to handle those things better either to avoid performance degradation or to make your code cleaner and more readable. It's also important to know the pros and cons from each method. At the end of the day you are the only one who knows your team and your projects to decide which one is the best for your project.
Learn More about Angular
Want to learn more about Angular? We have a few articles that you can read here on our blog.
Join Us
Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.
About Halodoc
Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek and many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.