Angular's @hostListener
is well known within the community. Rather unknown are the problems this might have on runtime performance and general application architecture. In general, there are three main problems with using the hostListener
decorator.
- Missing composability
- Performance issues
- Lacks configuration options
Before tackling those two problems more in detail, let's have a look at the example code used to demonstrate the problem.
To do so let's have a look at the following Stackblitz example, particularly the BoxComponent
:
Here we see an implemented drag'n'drop feature, using the @hostListener
decorator. In total, we registered 3 listeners.
- A
mousedown
event, which we are using to set a property signalling that our drag'n'drop is about to start. - A
mousemove
event, which calculates the position of the rectangle according to the mouse position. - Finally, we are using the
mouseup
event to signal that our drag'n'drop has ended.
Do note that we used document
as eventTarget. We needed that to handle fast mouse movements which might be out of sync with the position of the rectangle. One will notice that when moving the mouse very fast, that one is out of the rectangle element, which would stop our drag'n'drop.
Problems
Let's have a more in-depth look at the problems listed above.
Missing composability
Taking a look into the code, we will notice that we set the property isClicked
to true
as soon as the mousedown
event happens. We use that property to perform an early return inside of the mousemove
event handler to stop this function from execution. This is the only way we can compose those two events, which is quite expensive because this mousemove
function is still executed with every mouse movement. In terms of composition, this drag'n'drop feature is fairly straight forward. There are several much more complex event composition scenarios, which become extremely difficult when using the @hostListener
decorator.
Performance issues
This problem is mostly the resolution of the missing composability. The problem here is that we register the 3 event listener, mentioned above, for every component instance, even though it's impossible to drag'n'drop multiple rectangles at the same time. Therefore what we should aim for is, that only the mousedown
event listener is registered for every component and just when this event happens we register the other events accordingly. Doing all this logic within the event listener function is a lot of work and also decently complex. Additionally, there is currently no way to disable a registers @hostListener
function. This is also the reason why the code example above constantly listens to mouse move events, even though they are not relevant if there isn't a rectangle selected before.
Lacks configuration options
Usually, the addEventListener
provides an argument for configuration options (the description below is copied from the MDN web docs):
-
capture: A
Boolean
indicating that events of this type will be dispatched to the registeredlistener
before being dispatched to anyEventTarget
beneath it in the DOM tree. -
once: A
Boolean
indicating that thelistener
should be invoked at most once after being added. Iftrue
, thelistener
would be automatically removed when invoked. -
passive: A
Boolean
which, iftrue
, indicates that the function specified bylistener
will never callpreventDefault()
. If a passive listener does call preventDefault(), the user agent will do nothing other than generating a console warning.
One can clearly see that those configuration options are very powerful. For sure, one probably don't need to use them for every case. But especially for heavily event-oriented features this configuration options are key. If we take a look at the offical Angular documentation we will see, that we are not able to specify these configuration parameters, when using the hostListener
decorator.
Alternative Approaches
We have two different approaches to tackle the problems described above. Depending on your knowledge some of them are more or less complex. Let's have a look!
Using addEventListener
Theoretically one could register nested event listeners. Therefore we could use the addEventListener
function to register the event listeners.
Looking at the code example one will notice that this is fairly complex. Especially because we need to take care of registering and unregistering the nested event listeners. Even if all of the problems described above can be solved with this approach, In my personal opinion, I think that this is a very complex and hard to understand solution.
Using fromEvent
The second alternative approach would be using the RxJS fromEvent
operator. RxJS shines when it comes to composition of event-oriented code.
Having a look at this code, one will notice that just looking at the lines of code that this is the smallest approach. I have to admit that one needs to be familiar with RxJS to understand and write such code. It's not really intuitive, but therefore RxJS takes care of registering and unregistering the event listener for us. Additionally, we have many more opportunities regarding composability. That's one of the key benefits of using RxJS when dealing with event-oriented code.
If you want to understand the used operators you can have a look at the following blog posts:
Summary
The @hostListener
decorator is handy if we just want to listen to single events and don't rely on any kind of composition. Everything that involves a certain event composition should be implemented by using one of the other approaches listed above. In general, @hostListener
lacks features that are necessary when dealing with event composition. It completely misses cancellation options and any kind of composability. Those features are crucial when building heavily event-oriented features.
When you are used to RxJS you should probably use the fromEvent
operator to perform any kind of complex event handling. If RxJS is not your preferred technology, maybe using plain old addEventListener
might be a viable option for you.
Disclaimer
This blog post aims to elaborate on different approaches to deal with event composition. It never intends to blame or hurt someone who was involved in the design or implementation of the @hostListener
feature. I personally appreciate any work that was put into that.
Top comments (14)
It is probably better to use Renderer2 for listening instead of directly accessing the native element.
I think so. Renderer2 have
listen
method to do that.Oh thanks a lot for this feedback. I didn't know that before. I'll update the blog post today
Renderer2 listen method doesn't have params that have addEventListener. Actually it's the same as @HostListener.
Regarding the "The problem here is that we register the 3 event listener, mentioned above, for every component instance, even though it's impossible to drag'n'drop multiple rectangles at the same time." Actually this approach allow us to dag'n'drop multiple elements at once. Managed to do that 4 times on 2 different computers when testing how the code decides which rectangle to pick up when they overlap.
Good example that shows that Angular does not fully embrace RxJs Technology (It even hides it with EventEmitter, Input -> SimpleChanges). Now Angular team even takes into considerations to replace RxJs with Promises (ngconf 2022).
Angular's big merit on Web Platform is that it made Typescript and RxJs known to the Web Community. These two Technologies are so fundamental that they will outlive Angular.
Nice post! I recently started using fromEvent, cool stuff can be done with it also!
I started using it, for debounced input search, to not perform more requests than needed.
For observing inputs, you might consider using reactive forms and subscribing to the valueChanges observable on the form or formControl in question.
Ohh true, I did not consider this approach, I didn't like to have to pass in the element reference to
fromEvent
, I don't usually like manipulating elements directly so the reactive form could be a really nice solution.Thanks for the info!
That won't help with
enter
key which is often used.Thanks for article!
Btw takeUntil(mouseDown$) will complete the sequence. Should we add repeat() to ressurect it ?
the takeUntil is applied to the inner stream and we don't want this to repeat. It is going to be recreated with the next notification coming from the fromEvent. The indention is propably a little bit missleading
Ah, yes! Missed that it is for inner observable, thanks
You can solve all of these problems using Angular's Event Plugins and still use HostListener. It is the cleaner and clearer way of writing listeners in Angular.
Your RxJS code is broken, btw.