Subscribing to Observables in Ongoing Angular Lifecycle Hooks
How you should treat subscriptions in ongoing lifecycle hooks such as OnChanges without leading to performance issues

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