Event listeners with barba.js and the weird bug that came with it

As a front-end developer, you might think you know a thing or two about event listeners. That’s what I thought as well, until I encountered a weird bug that made me question my JavaScript skillset altogether. If you’re interested in digging a bit deeper into event quirks, this post is for you. The results might surprise you.
Geschreven door Tijs Verellen

Setting the scene

For one of our projects we used barba.js, a small library that makes page transitions smooth and snappy. It basically makes your site feel like a single page application. 

The library itself has a very intuitive api and the page transitions work great out of the box. The way barba.js works is that it only loads up the inner content of your body via an async fetch request. The main challenge we encountered with this type of page loads is making sure our own custom JavaScript gets initialised after each content insertion. This required some extra thought.

So we created one method that holds all of our JavaScript functionality, executes it on every page load, but also executes it using the ‘after’ hook (a method called ‘after the page transition’) from barba.js. This is what it looks like:

The bug

For one of our pages we implemented scroll functionality. This is done by using an event listener, which typically goes like this:

After a regular page load everything worked as expected. But after a barba.js page transition the event listener would be initialised a second time. Because there was no actual page load, the window object now had 2 identical on scroll callbacks, which completely broke the functionality we were trying to add.

Searching for a fix

So, how do you remove an event listener? This could have been done by completely overriding the ‘on scroll’ hook on the window object, which would have solved the problem. But this is a very bad way of adding functionality to the window object. By defining the window’s ‘on scroll hook’, all the other ‘on scroll functionality’ would have been replaced by this one callback. What if we wanted to change something else on scroll later? Clearly, it was not exactly what we were looking for.

What we needed to do was remove the previous event listener before we created a new one. This is made possible by using the ‘removeEventListener’ method. It requires you to pass the exact same event name and callback function to the ‘removeEventListener’ method that you used to declare the event listener with ‘addEventListener’.

This is what adding and removing an event listener would look like in a project similar to ours:

At first glance this looks to be correct, but what you see above this line won’t work. The event listener callback function will overwrite the ‘this’ keyword every time you’d use ‘this’ to access a class variable in ‘this.handleOnScroll’. And it would fail because of it. You can solve this issue by specifically binding ‘this’ to the callback function, like so:

Looking good! Now we’re able to add and remove event listeners. Let’s refactor a bit and set the event listener callback function to a global ‘window.onScrollCallback’ variable, so we can access it later in another instance of our class. Before adding an event listener, we check if the global variable exists. And if it does, that means the event listener is active, so we remove it from the window object:

Turns out the scroll events were still being duplicated. I checked if the global callback function existed in the second ‘SomethingOnScroll’ class instance, and it clearly did. Though it would keep adding new event listeners without removing any. So, as it turns out, the callback functions are not the same after all. Or are they? 

The fix

I turned the event listeners documentation upside down, added console.log outputs everywhere and read more Stack Overflow posts on the subject than I’m comfortable sharing. I was convinced I had missed some vital knowledge about event listeners. And then, it finally hit me:

I thought I was adding and removing the same callback function because I was using the same global variable to declare it. I was wrong, because using the ‘Function.prototype.bind’ method creates a new instance of that function with the ‘this’ keyword set to the declared value. (Documentation)

I was adding an altered version of the globally declared callback function. That’s why it wouldn’t remove the event listener later. It simply couldn’t match both functions. Binding ‘this’ to the function, while declaring the global callback, fixed the problem entirely.

Learnings

While solving this problem, I was focused on understanding event listeners because I was sure the problem originated from my lack of specific knowledge about them. Turns out I did know how they worked, and I simply overlooked the processes behind the ‘Function.prototype.bind’ function. 

In this blogpost I tried to simulate my tunnel vision for you. The entire story is built around event listeners and how they work, when in reality I was looking in the wrong place for a solution. 

Don’t get lost in the first explanation you come up with for an existing problem. Take a step back, zoom out and go over the flow of the program - step by step. That is how you solve problems faster and more efficiently.