Why do you bind?

Have you ever run into a problem where you have added an event listener using bind, but the event handler continues to execute even though you have removed the listener? If yes, read on.

A few months ago, when I was working on one of the React components, I added an event listener to the document object in the componentDidMount life-cycle and removed it in the componentWillUnmount life-cycle method. When I was checking the functionality, I found that the event handler was executing even after the component was no longer present. I checked the code, put the debugger, everything was in proper order. The question was, why did the event handler keep on executing after removing the event listener?

To get to the bottom of this, first, we’ll have to look at how does the bind functionality works?

In general, the use of bind is to provide the context inside a function during the function execution.

Here is an example:

1.  var obj = {
2.    name: "Jim Halpert",
3.    printName: function () {
4.      console.log (this.name || "No Name");
5.    }
6.  }
7.  var printName = obj.printName;
8.  
9.  obj.printName ();    // "Jim Halpert"
10. printName ();        // "No Name"

When at the line we execute the function, the execution context is “obj”, but in line 10, it’s not. The reason is when we do the assignment in line 7 we assign just the value of the function. Something like this,

var printName = function () {
    console.log (this.name || "No Name");
}

When we invoke this function, the current scope becomes the global scope. It has no idea about the “obj” object. Therefore it cannot find the property “name”, as we have not added to the global scope.

How to solve this?

Here comes the bind. We need to bind the function to a scope. Here is how to do it. Let us rewrite the statement of line 7.

7.  var printName = obj.printName.bind (obj);

So every time, we invoke the printName function, it’s scope will be obj. The value of this will be obj, and we have a property name for object obj.

However, it creates another problem, the issue we talked about early in this post. So let’s say, we have a module/page and requires some keyboard shortcuts. What we do is, we add a keyboard event to the document, and based on the key, we map actions. For example,

var _this = this;
document.addEventListener ("keyup", function (event) {
    switch (event.which) {
        case 48:
            _this.zeroKeyPressed ();
            break;
        .
        .
        .
        .
        default:
            _this.doNothing ();
            break;
    }
});

While this code has nothing wrong in it, what we do in a large code base, we write the code in different parts. What I mean by that is, all initialisations will be in a function (say: function init(){}), all DOM references are store using another function (say: function cacheDom () {}), all event bindings in one function (say: function bindEvents () {}), and so on.

It’s not a rule, but it’s a good practice. It makes the code more readable and understandable for fellow developers. So as part of good practice, we do not (usually) provide the event listeners inline. We could do it.

As a standard practice, we add the function declarations as event listeners. Here is a rewrite of the document key handler.

var myModule = {
    .
    .
    .
    .
    bindEvents: function () {
        document.addEventListener ("keyup", this.documentKeyHandler.bind (this);
    
    },
    documentKeyHandler: function (e) {
        switch (event.which) {
	    case 48:
                this.zeroKeyHandler ();
                break;
            .
            .   
            .
        }
    }
    .
    .
}
// module ends here.

It also allows us to bind the event handler with the current scope and we no longer need to keep track of “this” using “_this”. Second, we do not have an anonymous function as an event handler (or do we?), and we can use the event handler function for some other purpose. For example, invoking the fake event handler, or say, for simulating the key press event. Let’s get back to the problem.

What happens if we switch the module/page?

The document key handler will no longer have a valid environment. Because the key handler will try to do something with the components which are not part of the current page, and that might result in an error.

An easy way to deal with this scenario is to unbind the event handler when we leave this module. The following code should do the trick,

document.removeEventListener ("keyup", this.documentKeyHandler);
// or
document.removeEventListener ("keyup", this.documentKeyHandler.bind (this));

Sidenote: Another advantage of using function declaration as event handler is, we can remove/detach selected functions. In vanilla JavaScript, we have to mention the function name that we need to detach while removing event handlers. In jQuery we can use the “off” method and, it will remove all event listeners. So doing an “off” will remove all event listeners, but with function declarations, we can remove selected functions. Makes sense?

We still have to address one last issue though. I guess I have made my case here against using anonymous functions in such scenarios.

  • It is impossible to track them, as they are anonymous 🙂
  • It is impossible to re-use them.

Unfortunately, bind creates an anonymous function, with the given scope. So, we are kind of back to square one, if we use the bind method. That’s the reason my document’s event handler kept on running even after the component unmount.

Solution?

It has an easy solution though. We just need to keep a reference and use that instead of doing bind inline.

var myModule = {
    .
    .
    .
    init: function () {
      this.documentKeyHandler = this.documentKeyHandler.bind (this);
    }
}
// module ends here

Now every time we invoke documentKeyHandler it will execute the same anonymous function which was created by the bind method.

PS: Few simplifications can be made in the code blocks using ES6’s fat arrow function to keep the context, as the fat arrow functions do not have their own scope. But this post is about bind, so I have not used fat arrows.