Event Listeners – Remember to remove them!

Click, focus, keydown, resize, scroll and many many more… JavaScript EventListeners are a part of every JavaScript application or framework. Without them JavaScript as a language would be already a thing of the past. This short post will describe one common mistake developers make when it comes to handling event listeners.

One of the goals of Single Page Applications (SPA) is to provide best possible user experience to a user. We have nice transitions for navigating between pages, nice and subtle content pre-loaders that update content smoothly or save data to the server without full page refresh. It is all great, but last four words of previous sentence are a key here – “without full page refresh”. What is good about page refresh is that it flushes all JavaScript objects, variables etc. In SPAs we need to be really careful how we are dealing with some of the JavaScript functionality, one of which are EventListeners.

I will use Create React App (along with React-Router) to quickly create small SPA where I will create some event listeners. Then, using Google Dev Tools, I will show you how to trace event listeners and how to correctly remove them so there is no memory leaks. Although, I will be working with React and Google Chrome, same principles apply to any JavaScript or web browser.

Create React App and add React-Router

Lets create our sample application and add React-Router. You can also grab code for this post from GitHub

create-react-app EventListeners
// cd EventListeners
npm install React-Router@latest --save

Next, create a components folder in src folder and add four files: ConsoleLogger.js, PageOne.js, PageTwo.js and PageThree.js and then modify App.js file so it is using React-Router as a root component (please use Github link to get all the files as I want to focus purely on JavaScript EventListeners).

We will focus on ConsoleLogger.js, here is a starting point:

import React, { Component } from 'react';

export default class ConsoleLogger extends Component {
  constructor(props){
    super(props);

    this.state = { id: Math.random() };
  }

  componentWillMount(){
    document.body.addEventListener('click', this.displayMessage.bind(this));
  }

  displayMessage(){
    // don't finish reading here, it is not an end :)
    console.log('Event Listeners from ConsoleLogger with id:', this.state.id);
  }
  render() {
    return Console Logger
  }
}

The job of this component is to simply add click event listener to a body a page. Whenever user clicks anywhere on a body, there will message displayed in Dev Tool console. It is rendered more than time in some of the pages.

So the run the app

npm start

Tracing Event Listeners

Open Dev Tools and observe what is logged to the console. Click around, navigate to other pages, click again. Probably you’ve noticed that something strange is going on, when you click only once there  are more than few message in console, how is this possible? Well, so far we were adding event listener, but didn’t remove it. In DevTools select  Elements tab click on a body tag and then select Event Listener tab on the right hand side. You will see there a click event and when you expand it, there will few body items listed. It means that there is that many click events listener on the body. If you expand body, you will see that event handler is our displayMessage function. So, if you can see something similar in your applications, please remove as it will create serious memory leaks overtime.

DevTools console

Fixing un-removed EventListeners

So, how fix it? In ReactJs world, you can take advantage of a life-cycle methods, so when component is unmounted(i.e. users navigates to different page). So lets add following code:

componentWillUnmount(){
  document.body.removeEventListener('click', this.displayMessage.bind(this));
}

Ok, so lest give it a shot now. Click around, navigate between pages and observe Dev Tool console. And? Same thing again but why? Well, look closer at what we are adding and removing from event listeners:

this.displayMessage.bind(this)

Because we are binding current context, function is ‘the same’, but its reference is different. There is a really simple fix to this. In constructor of our component we will create instance variable that will hold reference to the function that we want to add/remove to/from event listeners. And then we simply pass this new variable instead of this.displayMessage.bind(this). So your constructor of ConsoleLogger should look like this:

constructor(props){
  super(props)
  this.state = { id: Math.random() };

  this._displayMessage = this.displayMessage.bind(this);
}

And your componentWillMount and componentWillUnmount should look like this:

componentWillMount(){
  document.body.addEventListener('click', this._displayMessage);
}

componentWillUnmount(){
  document.body.removeEventListener('click', this._displayMessage);
}

 

And now, there won’t any more un-removed event listeners.

I hope you enjoyed this post, there is more to come!

Leave a Reply

Your email address will not be published. Required fields are marked *