Techniques for removing Event Listeners

Techniques for removing Event Listeners

Using AbortController() to abort is so clean

·

6 min read

Event listeners in Javascript are a powerful way for the code that we write to plug into user interactions, page events, and a whole host of useful events. Adding event listeners into our code is straightforward and can help us to do some really interesting things. However, removing event listeners can be a little more tricky. You can almost guarantee that we are all using web apps that are running lots of superfluous code just because some event listener hasn't been removed properly. Most of the time, neither the user nor the developer notices anything so the Event Listener will stay hidden forever.

In some scenarios, not removing event listeners can cause lots of unpredictable behaviour, making our code really difficult to debug. This is especially true as our code bases get bigger and we have lots of different classes and objects that might register their own event listeners.

In this article, we will explore a simple Square class that adds a red square to the DOM. When the red square is shown in the document, the class should also use the user's mouse actions to determine what color to set as the main backgroundColor of the app. If the user hovers over the app, the backgroundColor will be changed to yellow.

Moving the mouse in and out of the app before the red square is drawn to the screen should do nothing as there have not been any event listeners registered yet to handle this behaviour. Once the red square is available, the background color of the app should change as we move in and out of the app. Finally, once the red square has been removed from the document, we have completely destroyed this class and the reference to it. However, the event listeners are still there and have escaped garbage cleanup.

Trying with removeEventListener()

This is probably the most obvious approach to remove any listeners, but also the approach that is going to give you the most headaches.

Let's understand how the EventTarget.removeEventListener() method works. Simply put it removes an event listener from the target that was previously registered with EventTarget.addEventListener(). The event listener is identified using a combination of the event type, the event listener function and the optional options parameter.

removeEventListener(type, listener)
removeEventListener(type, listener, options)
removeEventListener(type, listener, useCapture)

For EventTarget.removeEventListener() to remove an event listener, the parameters passed to the method have to be exactly the same as the parameters that were passed to the original EventTarget.addEventListener().

Here we have to be really careful. Looking at the modified code below, you can see that we have added two removeEventListener() method calls to the destroy() method.

Adding removeEventListener() to the destroy() method has not removed the event listeners as we still get a yellow background each time we mouse over the app

At a first glance, it looks as though we have used exactly the same type and event listener function to add and remove the event. However, this is not really the case as the functions that are added in the registerListeners() method are in fact different functions from the functions that we are trying to remove in the destroy() method.

The way to get around this problem is by either using a class method or property as the event listener function.

setYellow(event){
    appEl.style.setProperty('background color, 'yellow');
 }

or

setYellow = (event) => {
    appEl.style.setProperty('background color, 'yellow');
 }

and then we can do the following to correctly remove the event listeners.

appEl.addEventListener('mouseenter', this.setYellow);
appEl.removeEventListener('mouseenter', this.setYellow);

If we were not doing all this inside a class and just inside/outside a function then we could have done it as follows.

function setYellow(event){
    appEl.style.setProperty('background color, 'yellow');
 }

or

const setYellow = (event) => {
    appEl.style.setProperty('background color, 'yellow');
 }

and then removed the event listeners with

appEl.addEventListener('mouseenter', setYellow);
appEl.removeEventListener('mouseenter', setYellow);

The sometimes useful {once: true} option

This pretty much speaks for itself.

EventTarget.addEventListener(type, listener, {once: true});

When we use the {once: true} option when adding an event listener, the listener will be invoked at most once and immediately removed as soon as the event is invoked.

This is really useful for things like submit buttons, but the kind of interaction that we are implementing in this example won't be very helpful.

Using {once: true} gives us the desired behaviour once and only once

Removing the element from the DOM

It is worth noting that if the event listeners have been added to an element that we remove from the DOM, then the event listeners will be automatically destroyed anyway.

Similarly, it is possible to manually remove an element from the DOM and then add it back in without its event listeners attached. We can use the Node.cloneNode() method to clone an element which will create a copy of the original element but will not carry the event listeners from the original element. Removing the original element and replacing it with the clone will have effectively removed all the event listeners from the element.

Whilst, this is an approach that might feel a little heavy-handed, there might be scenarios where this becomes very useful because we are trying to control a part of the code base that is just not very easy to access or is controlled by third-party libraries.

Using AbortController() with addEventListener()

If you're anything like me, you have probably only heard about using AbortController() for cancelling a network request. However, after hearing a developer at work mention being able to use AbortController() to cancel event listeners, thought I would dive in and try out an example.

The approach is by passing in a control signal to the options when an event listener is added.

const controller = new AbortController();
myElement.addEventListener('mouseenter', () => {}, {signal: controller.signal});
controller.abort();

There are several benefits to using this approach.

Firstly, we don't need to pass a function reference to the addEventListener() method which means that we don't need to keep track of which listeners were attached. Even where it can be nice to use a function reference to keep our code clean or reusable, we might be trying to cancel the event listener from a different point in our code base which can make this relatively simple task very difficult.

Secondly, we can use a single method to cancel multiple listeners attached to the same document element or different elements.

const controller = new AbortController();
const {signal} = controller;
myElement.addEventListener('mouseenter', () => {}, {signal});
myElement.addEventListener('mouseleave', () => {}, {signal});
otherElement.addEventListener('click', () => {}, {signal});

controller.abort();

Let's use this approach to change how our Square class registers its event listeners.

This approach feels as though it gives us the most flexibility in adding and removing event listeners. I'm gonna try and get in the habit of using this whenever I register event listeners in the future.

What are your thoughts?

Share what you think in the comments.

Do you think we should all abandon removeEventListener() and just use AbortController()?