Home > OS >  Javascript class instance method does not work with onClick callback event in react
Javascript class instance method does not work with onClick callback event in react

Time:10-24

I was trying to update my state variable when the button is clicked on my page. But I see that I cannot use react class component methods. Here is the minimum reproducible example.

import React, { Component } from 'react'

export class Name extends Component {
    constructor(){
        super();
        this.state = {
            name : "Farhan Ahmed"
        }
    }
    clickMe() {
        this.setState({
            name:"Ahmed Farhan"
        })
    }

    render() {
        return (
            <div>
                <h1>{this.state.name}</h1>
                <button className="btn btn-success" onClick={this.clickMe}>Change Text</button>
            </div>
        )
    }
}

export default Name

Error :

TypeError: Cannot read properties of undefined (reading 'setState')

But when I replace the same with arrow function it works for me.

My question is why didn't regular class method work in this case why do I need to replace the same with arrow function?

CodePudding user response:

When you try to access this keyword without binding the function, this keyword is undefined. so, you need to either bind that function in constructor or use an arrow function syntax which ensures this is bound within that function. You can check documentation of bind method method and Arrow Function

CodePudding user response:

Button is not clicked, but function will run while the component is rendering.

onClick={this.clickMe}  

And this;

onClick={() => this.clickMe} 

Function only works when the buttons are clicked.

Read this: https://beta.reactjs.org/learn/responding-to-events

CodePudding user response:

In the docs found here https://reactjs.org/docs/handling-events.html this error is explained, along with possible solutions. If you're not using the experimental "public class fields syntax" the docs refer to, you can either bind your function, or use an arrow function:

With bind

onClick={this.clickMe.bind(this)}

Arrow function

onClick={() => this.clickMe()}

These are the most common (that I've seen personally), but the docs provide more solutions as well.

Edit

Someone pointed out that OP is asking "why", not necessarily "how do I fix it?" So from the docs linked above:

You have to be careful about the meaning of this in JSX callbacks. In JavaScript, class methods are not bound by default. If you forget to bind this.handleClick and pass it to onClick, this will be undefined when the function is actually called.

This is not React-specific behavior; it is a part of how functions work in JavaScript. Generally, if you refer to a method without () after it, such as onClick={this.handleClick}, you should bind that method.

CodePudding user response:

This problem is not specific to react but actually related to how Javascript works in general.

this functions a little differently in javascript. Since React is a javascript library, this in React follows the same concept.

In javascript, the behaviour of this keyword is based on how the function is called. this holds the reference to current execution context in javascript. Depending on how a function is called, this can refer to different objects.

  • If a function is called using an object like obj.functionName() then this will always return a reference to an object

  • If a function is called as a stand-alone function like functionName() then this will return a reference to window object but if strict mode is enabled, then this will return undefined.

  • If a function is a class method called from a callback like callback = obj.functionName(); callback(); then this will return undefined.

Now we are more interested in knowing about the third point where we are setting our class function to a callback onClick.

Lets understand it with an example

Consider the class Rectangle with following methods

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  area() {
    console.log(this);
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
}

Now here when we do

const square = new Rectangle(10, 10);
square.area()

It gives us the expected result

Rectangle {height: 10, width: 10} /* value of this */
100

Now when we store the same in a call back and call the callback function

callback = square.area;
callback()

This gives us error :

undefined  /* value of this */
VM106:9 Uncaught TypeError: Cannot read properties of undefined (reading 'calcArea')
    at area (<anonymous>:9:17)
    at <anonymous>:1:1

As you can see the value of this is undefined. This is because the area method is rescoped and ends up losing its context i.e. its this keyword reference. Same thing is happening in your case as well.

Solution :

Now try using an arrow function or binding the same function to the class, you will be able to call the callback successfully.

Using arrow function

area = () => {
    console.log(this.width, this.height);
    return this.calcArea();
  }

Advantage of using arrow function is that it doesn’t rescope “this” keyword which eliminates the need to bind “this” in the constructor

By binding the function in the constructor

constructor(height, width) {
    this.height = height;
    this.width = width;
    this.area = this.area.bind(this);
  }

Now calling the same using callback

const square = new Rectangle(10, 10);
callback = square.area;
callback()

will give us the result

Rectangle {height: 10, width: 10, area: ƒ}
100

This is a good approach to bind event handlers but it will be a hectic task to repeat the same set of steps for each event handler.

  • Related