X

The easy way to binding event handlers in Reactjs

Dung Do Tien Sep 10 2020 954
We have many ways to the binding event handler in reactjs, In this article, I will guide some easy way to binding event handlers to fire some events such as click, focus, lost focus, hover ...

1. What is bind event handlers in Reactjs

When building applications in React using Class Component, you might have known about passing an event handler to a component:

<button onClick={this.handleClick}>Click</button>;

But if you want to access to attributes of parent component such as this.state or this.props, you also need to bind the function.

Bind in the constructor:

export default class Foo extends Component {
    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    } 
    handleClick(e) {
        // Do something here
    } 
    render() {
        return <button onClick={this.handleClick}>Click</button>
    }
}

Bind in render():

export default class Foo extends Component {
    handleClick(e) {
        // Do something here
    }

    render() {
        return <button onClick={this.handleClick.bind(this)}>Click</button>;
    }
}

This article will help you understand more about binding functions into the component instance.

2. Why do we need to bind event handlers?

Because of the way this works in JavaScript, nothing to do with React or Jsx. Put in different place, this play different roles and refer to different objects, in this article, we only concern two ways: 

-    In a method, this refer to the owner object.
-    In a function or stand alone, this refer to global object.

In JavaScript, these two code snippets are NOT the same:

Object.method();

var methodv2 = Object.method;
methodv2(); // methodv2() becomes a function.

The value of this inside a function depends on how that function is invoked. Let’s go through examples to have a clear view on how this works in JavaScript.

2.1 Creating a bound function.

First, use this alone would refers to ‘window’ object in the browser, created an attribute named “a” and signed a value of “8”.

this.a = 8; // 'this' refers to global 'window' object in brower.
console.log(window.a); // print 8

Next, we create an object with the attribute “a” with a value of “64”, attribute “b” with value of “65” and a method which return that attribute, this in this case stand in a method and refer to the parent object “obj”:

const obj = {
    a: 64,
    b: 65,
    getA: function () {
        return this.a; // 'this' refers to 'obj' object.
    },
    getB: function () {
        return this.b; // 'this' refers to 'obj' object.
    },
};

Let’s test the output by logging the result in console: the returned value of method getA(), getB() and attribute “a” of object “window”:

let testA = obj.getA();
let testB = obj.getB();
console.log(testA, testB); // print 64 65

The two functions/methods invoked inside the object scope so this would refer to “obj”
Now we use the two methods reference to two new functions getAv2 and getBv2 to see what will be returned if we don’t bind the object:

const getAv2 = obj.getA; // ~ function getAv2() { return this.a; }
const getBv2 = obj.getB; // ~ function getBv2() { return this.b; }
testA = getAv2();
testB = getBv2();
console.log(testA, testB); // print 8 undefined

As you can see, the function now invoked in the global scope, this point to global – “window” object. We expected getAv2() and getBv2() would return the value of attribute “a” and “b” of object “obj” but they returned “8” and “undefined”. This happened because those functions get invoked in the global scope, the value of this falls back to default binding, which is global/window object. Thus getAv2() returned the attribute “a” of “window” object and getBv2() return “undefined” due to there was no attribute “b”.

To solve this, we simply bind the object to functions. With the bind() function, a new bound function is created (or exotic function object – ECMAScript 2015 term) which wraps the original function object. Calling the bound function will return as it happens inside the original function or object.

const getAv3 = getAv2.bind(obj);
const getAv3 = getBv2.bind(obj);
testA = getAv3();
testB = getBv3();
console.log(testA, testB); // print 64 65

2.2 Passing functions as callbacks to another custom function.

Passing functions as callbacks to another custom function, built-in JavaScript setTimeout() or third-party libraries or frameworks.

Passing functions as callbacks work similarly to passing event handlers from child class components to higher order components. Let’s make an example with setTimeout to see how it works in plain JavaScript.

Step 1: Create an object with an attribute name which has the value of John and a method which log the attribute value to console:

const obj2 = {
    name: "John",
    getName: function () {
        console.log(this.name);
    },
};
obj2.getName(); // print John

Step 2: Implement setTimeout() function and set getName() method as callback:

setTimeout(obj2.getName, 5000);

Run the code and after 5 seconds, it returns undefined. Why is this happen? Take a look at the implementation of setTimeout() function:

function setTimeout(callback, timeout) {
    //wait for 'timeout' milliseconds
    callback();
}

When we replace callback function with obj2.getName, it runs similar to use the getName method reference callback, thus make the callback function invoked at global scope and return undefine. To solve this, we just need to bind the object to the callback function so the pointer this will point to obj2.

setTimeout(obj2.getName.bind(obj2), 5000);

3. Binding event handler in ReactJs.

Why we only need to bind event handler, not render() method or component lifecycle? Because we need to pass the event handler function to other components so bind() method would help the function to keep its original context. Other methods only occur within the block of the class component so ot is not necessary to bind.

The example below will help you visualize the theory. Let’s say we want to create a button which change the text every time we click it.

Step 1: Create the component and our button, set the switch clicked to “false” in the state.

import React, { Component } from "react";
export default class ToggleButton extends Component {
    state = { clicked: false };

    render() {
        return (
            <div>
                <button>
                    {clicked ? "ON" : "OFF"}

                </button>
            </div>
        );
    }
}

Step 2: Set the clickHandler function as onclick event handler so every time we click the button, the function will be called.

<button onClick={this.clickHandler}>

Step 3: Create clickHandler function to toggle the switch clicked by changing the state.

clickHandler() {
    this.setState((prevState) => ({ clicked: !prevState.clicked }));
}

The complete code will look like this:

import React, { Component } from "react";
export default class ToggleButton extends Component {
    state = { clicked: false };

    clickHandler() {
        this.setState((prevState) => ({ clicked: !prevState.clicked }));
    }

    render() {
        const clicked = this.state.clicked;

        return (
            <div>
                <button onClick={this.clickHandler}>{clicked ? "ON" : "OFF"}</button>
            </div>
        );
    }
}

Looks like we good to go. Run command “npm start” and everything rendered. But when we click the button, we get an error:

We are getting this error because when our clickHandler was called by onClick, this was not defined, in other words, the method lost its component instance.

To solve this, we simply need to make this point to the component by using bind() method so this.clickHandler can be passed to other components. We can do this in the constructor:

constructor(props) {
    super(props);
    this.clickHandler = this.clickHandler.bind(this);
}

Everything works exactly as expected now. The complete code looks like this:

import React, { Component } from "react";
export default class ToggleButton extends Component {
    constructor(props) {
        super(props);
        this.clickHandler = this.clickHandler.bind(this);
    }

    state = { clicked: false };
    clickHandler() {
        this.setState((prevState) => ({ clicked: !prevState.clicked }));
    }

    render() {
        const clicked = this.state.clicked;
        return (
            <div>
                <button onClick={this.clickHandler}>{clicked ? "ON" : "OFF"}</button>
            </div>
        );
    }
}

4. Other ways to bind event handlers and tips.

When binding event handlers, a common way we usually see is binding in the constructor:

constructor(props) {
    super(props);
    this.clickHandler = this.clickHandler.bind(this);
}

But there are many other ways to do this. Let’s find out.

4.1 Using the arrow function.

import React, { Component } from "react";
export default class ToggleButton2 extends Component {
    state = { clicked: false };
    clickHandler = () => {
        this.setState((prevState) => ({ clicked: !prevState.clicked }));
    };

    render() {
        const clicked = this.state.clicked;
        return (
            <div>
                <p>Button 2</p>
                <button onClick={this.clickHandler}>{clicked ? "ON" : "OFF"}</button>
            </div>
        );
    }
}

This is an experimental public class field feature in JavaScript which allows you to use function arrow syntax in your class. This way, the event handler bound lexically and automatically binds to the component instance. This means that it uses the context of the enclosing function — or global — scope as its this value.

4.2 Bind in Render.

import React, { Component } from "react";
export default class ToggleButton3 extends Component {
    state = { clicked: false };
    clickHandler() {
        this.setState((prevState) => ({ clicked: !prevState.clicked }));
    }

    render() {
        const clicked = this.state.clicked;
        return (
            <div>
                <p>Button 3</p>
                <button onClick={this.clickHandler.bind(this)}>{clicked ? "ON" : "OFF"}</button>
            </div>
        );
    }
}

We can also bind the event handler directly inside the component, but using bind() in render method can harm the application performance because it will create a new function each time component render, in this case, is every time we click the button.

4.3 Using arrow function in Render.

import React, { Component } from "react";
export default class ToggleButton4 extends Component {
    state = { clicked: false };
    clickHandler() {}
    render() {
        const clicked = this.state.clicked;
        return (
            <div>
                <p>Button 4</p>
                <button
                    onClick={() => {
                        this.setState((prevState) => ({ clicked: !prevState.clicked }));
                    }}>
                    {clicked ? "ON" : "OFF"}
                </button>
            </div>
        );
    }
}

We can also implement the function directly in the bracket using an arrow function, but this way has the same drawback as using bind() in Render, which creates a new function each time component render, so it may slow down the performance.

5. Conclusion.

In the Reactjs class component, the event handler when passed from a component to another will lose its context and falls back to default binding and set to undefined if it is not bound to its parent component. We can bind the component instance in the constructor of the class component and the event handler can be passed without worrying it will lose the original context.
Although using the arrow function is currently in the experiment period but it is still a good way to make your code cleaner since we can cut out the constructor and the two other ways seem to slow down the performance of the application.

THIS ARTICLE IS WRITTEN BY MINH QUAN. I WILL UPDATE AUTHOR IN THE FUTURE.