Better Programming

Advice for programmers.

Follow publication

Subscribing to Observables in Ongoing Angular Lifecycle Hooks

Zeev Katz
Better Programming
Published in
4 min readSep 3, 2020
A list of observables next to the Angular logo.
Photo by the author.

Before we start, let’s make it clear: Manual subscription in the components’ lifecycle hooks is the worst way to handle observables. If there is no other option and using the async pipe doesn’t work as expected, then you can keep reading to learn how to safely achieve this and avoid zombie subscriptions around your application that will eventually lead to unexpected memory leaks.

A Real-World Case

For our example, I chose to implement a directive that logs its host element events to some user analytics provider. For simplicity, I will not describe how the service was implemented but rather focus on the events registration in our directive.

So, let’s see what the directive looks like:

As you can see, this directive provides several configurable inputs such as events (indicates which events should be listened to) and properties (allows us to attach some additional properties to the logged events). properties isn’t required for the problem demonstration, but I promised a real-world example, no?

Now let’s go over the available methods. There are two methods used by ngOnChanges:

  • registerEvents registers to all the events in the given list and returns an observable that emits when one of them occurs.
  • logEvent logs the given event type and properties by using the user analytics service.

The sharp eyes among us have probably noticed that I used the untilDestroyed operator (by Netanel Basal) to unsubscribe the registered events subscription when our directive will be destroyed, but is it good enough?

The Problem

In one sentence, OnChanges is an ongoing lifecycle hook that is invoked by any input change. In our case, it will lead to a new subscription on every change of the events input.

Let’s walk through the directive flow with the following usage example and see the result of the sneaky problem in the current implementation of OnChanges:

  1. We used the directive somewhere in the application with the events that we want to log.
  2. Initially, isEditMode === false, so the events input is now ['focus'].
  3. OnChanges called and our directive registers to the ‘focus’ event.
  4. The user is focused on our text-editor component and the ‘focus’ event is logged to the analytics provider.
  5. The user clicks on some edit button, then (editModeChange) emits and now isEditMode === true.
  6. The events input changed to [‘contextmenu’, ‘focus’], OnChanges called again, and the directive re-registered to the new events list.
  7. The user is focused on the text-editor component and the ‘focus’ event is logged again.

Now, if we log the registered events subscription emissions, you will see that the ‘focus’ event is logged three times, where we expected it to be logged only twice. This happens because we have not unsubscribed the first registered events observable before registering to the new incoming events list.

So how can we fix it? Let’s open our toolbox, roll up our sleeves, and renovate this directive!

Solution #1: Unsubscribe

Yes, it’s as simple as that! Unsubscribe from the previous subscription before subscribing to a new one. In the same manner, as you unsubscribe observables a moment before directive destruction, you need to do the same in this case in order to throw the previous subscription.

See how it’s implemented in our directive:

Solution #2: Subject + takeUntil

Most of the time, unsubscribing observables manually is a great solution, but when a directive/component becomes more complex and the amount of observables increases, having a mass of subscription references will mess up our logic pretty quickly. In such a case, we’d like to find an efficient way to unsubscribe all of our observables at once.

We can achieve this by having a notifier Subject that tells us that the OnChanges lifecycle hook occurred. Then we can stop taking new emissions by completing the previous observable using takeUntil.

Solution #3: Rehooktive + takeUntil

The subject solution gave rise to me seeking a simpler and generic alternative that will work for any other lifecycle hook in Angular. So I took the challenge and implemented a fully decorative solution that reactive the Angular lifecycle hooks. Say hello to Rehooktive!

And here’s how easily it can work in our directive:

You shouldn't worry about unsubscribing on OnDestroy. Rehooktive will do the work for you automatically even if you’re working with other lifecycle hooks.

Another option for a full reactive solution is using the switchMap operator and mapping any emission of OnChanges, including an events input change to new registered events observables inside the directive’s constructor. Here is the result:

Summary

The proper way to handle subscriptions varies from case to case. One-time lifecycle hooks will behave differently than ongoing lifecycle hooks, so we need to be aware of that and act accordingly.

No matter which solution you choose, make sure that wherever you are subscribing to observables, you are unsubscribing to them correctly as well.

Thanks for reading!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Zeev Katz
Zeev Katz

Written by Zeev Katz

Frontend Leader at proteanTecs 👨‍💻 | ngze owner and leader 🚀

Responses (3)

Write a response