Mastering Subscription Handling in Angular

Angular Sep 22, 2023

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.

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:

Without unsubscribing, subscription are still active even when the component are destroyed.
Subscription are disposed when unsubscribing correctly.

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:

  1. 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 calling this.$destroy.next to make the Subject emit a value and this will trigger the takeUntil 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.

Understanding Angular Change Detection Strategy
As an Angular developer, sometimes we might be curious about how Angular detects changes in the data (model) and then renders them to the view. This is called Angular Change Detection Strategy.
Angular Strict Mode: Improving Performance and Reducing Errors
Angular’s strict mode allows developers to enhance application performance and reduce errors. Enabling strict mode catches potential errors early, improving code reliability and providing a strong foundation for scalable and high-quality Angular applications.
Angular Standalone Components: No NgModule Magic
Standalone components in software engineering solve challenges in creating fast and efficient web applications, especially with frameworks like Angular. This approach streamlines development, enhances code modularity, and leads to exceptional user experiences.
Advanced Angular concepts used in Halodoc
Halodoc.com is a rapidly growing website that caters to millions of ourcustomers across Indonesia. Hence, it’s quintessential to ensure that itcontinues to deliver superior performance and a great experience to ourcustomers at all times. One of the ways to achieve performance is using advancedAn…
Best Practices and Guidelines for web applications
This writeup is related to Angular to outline a use case in high demand from the web community to follow best practices we can use in our application to support a robust and highly scalable angular framework. Additionally, the blog discusses some general coding guidelines to make the app cleaner.

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.

Brilyan Aro

Do web stuff.