nonopolarity
nonopolarity

Reputation: 151126

How to solve the lost binding issue for event handlers in React?

In React, it looks like for a self-contained component, we are still experiencing the "lost binding issue":

The following React code in CodePen to change a from 123 to 456 doesn't work:

class Foo extends React.Component {
  state = { a: 123 };

  clickHandler() {
    this.setState({ a: 456 });
  }

  render() {
    return (
      <div>
        <h1>Hello, world! {this.state.a} </h1>
        <button onClick={this.clickHandler}>Click Me</button>
      </div>
    );
  }
}

ReactDOM.render(
  <Foo />,
  document.getElementById('root')
);

It can be solved by

  1. making it onClick={this.clickHandler.bind(this)}
  2. making the handler an arrow function

Is there any other way to write the code to handle the lost binding issue or to make the issue go away altogher? Is it possible to do in the constructor or componentDidMount() to bind all the methods to itself, something like (pseudo code) so that the issue of lost binding can be gone as far as the component is concerned?

for (methodName of this.allMethodNames()) { 
  this[methodName] = this[methodName].bind(this); 
}

Upvotes: 1

Views: 521

Answers (5)

Matt Carlotta
Matt Carlotta

Reputation: 19772

You can bind in the constructor. However, if you want "auto binding", I'd recommend using arrow functions since they inherit the lexical scope of this -- where this refers to the parent class versus an unbinded callback method that loses its lexical scope and this refers to the window.

In addition, I'd avoid binding this in the callback function because React has to recreate the function for each re-render (which can lead to an ever so slightly slower performance).

That said, you could do something like the example below -- although I don't recommend it, since not all of these methods would need to be binded to this if they weren't used as a callback in the render method (also Object.getOwnPropertyNames is not supported in older browsers):

import React from "react";
import "./styles.css";

class App extends React.Component {
  constructor() {
    super();
    this.state = { a: 123 };
    Object.getOwnPropertyNames(App.prototype)
      .filter(method => !["constructor", "render"].includes(method))
      .forEach(method => {
        this[method] = this[method].bind(this);
      });
  }

  handleSetClick() {
    this.setState({ a: 456 });
  }

  handleResetClick() {
    this.setState({ a: 123 });
  }

  render() {
    return (
      <div>
        <h1>Hello, world! {this.state.a} </h1>
        <button type="button" onClick={this.handleSetClick}>
          Click Me
        </button>
        <br />
        <button type="button" onClick={this.handleResetClick}>
          Reset
        </button>
      </div>
    );
  }
}

export default App;

Working example:

Edit "Auto" bind

What it looks like to "auto" bind (compiled): https://pastebin.com/dZE0Hdnn

What it looks like to use arrow functions (compiled): https://pastebin.com/e0xmh1fn

The main difference is that arrow functions instantiate the class versus being bound as a prototype to the class when bound in the constructor. This mostly affects inheritance, which you can read more about here. That said, React components don't usually extend from plain classes, instead they either extend from React's Component or PureComponent, and you won't be directly calling a Class.prototype, so take the article with a grain of salt (also, the article is very old and the performance numbers are outdated).


On a side note, you also bind using decorators, although support for decorators is hit or miss (since they're still in proposal stage -- or were, I haven't checked in awhile).

Upvotes: 0

Andus
Andus

Reputation: 1731

I believe there's no way to automatically bind every function you have to bind, simply because you are calling another component whenever you use onClick which makes your this won't work anymore (e.g. you have used a button in your example).

However, there are two other ways, you can either bind it in the constructor (similar to what you suggested to bind it in componentDidMount() but that won't work), like this:

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

Or you can either change your function to an arrow function if you do not want to use an arrow function in render method like this onClick={()=>clickHandler()}, which is no good because you'll create a new function instance every time render is called :

clickHandler = () => {
  this.setState({ a: 456 });
}

<button onClick={this.clickHandler}>Click Me</button>

I personally recommend the second way because you don't have to write one more line for the binding. More importantly, you can pass extra params directly from the clickHandler like this:

clickHandler = (addMore, addEvenMore) => () => {
  this.setState({ a: 456 + addMore + addEvenMore });
  // set a to 456 + 100 + 200
}

<button onClick={this.clickHandler(100, 200)}>Click Me</button>

With a binding in constructor, you cannot pass parameters, you have to do the binding when you pass the function to button, making the line clumsy, because the handler now describe multiple actions (the onClick action and the bind action) with the existence of the word bind:

<button onClick={this.clickHandler.bind(this, 100, 200)}>Click Me</button>

Upvotes: 0

Paul
Paul

Reputation: 1022

You can use ES6 arrow functions. They prevent you from having to call .bind(this) for every function.

clickHandler = () => {
  this.setState({ a: 456 });
}

<button onClick={this.clickHandler}>Click Me</button>

Upvotes: 0

bdanzer
bdanzer

Reputation: 57

If you want automatic binding you'll need to rewrite the onClick to something like this:

<button onClick={() => this.clickHandler()}>Click Me</button>

Otherwise you'll have to do it like this:

<button onClick={this.clickHandler.bind(this)}>Click Me</button>

One way around both of those is to use functional components where you could just pass the function with no issues. Here is your component turned into a functional component here: https://codesandbox.io/s/distracted-nobel-46wgf

import React, { useState } from "react";
import ReactDOM from "react-dom";

export default function Foo() {
  const [state, setState] = useState({ a: 123 });

  const clickHandler = () => {
    setState({ ...state, a: 456 });
  };

  return (
    <div>
      <h1>Hello, world! {state.a} </h1>
      <button onClick={clickHandler}>Click Me</button>
    </div>
  );
}

ReactDOM.render(<Foo />, document.getElementById("root"));

Upvotes: 1

serraosays
serraosays

Reputation: 7869

Your components needs a constructor, that's where binding is supposed to happen in class-based React:

class Foo extends React.Component {

  // Set up your constructor and explicitly pass props in
  // React will pass props in automatically but this just makes it easier to read
  constructor(props) {

    // `this` points to `Foo` now that you have a constructor
    // it will also make the state obj available to your custom methods
    this.state = { a: 123 };

    // inside of your constructor, bind your custom methods to `this`
    // if you have more than one, bind them all here
    this.clickHandler = this.clickHandler.bind(this)
  }

  clickHandler() {
    this.setState({ a: 456 });
  }

  render() {
    return (
      <div>
        <h1>Hello, world! {this.state.a} </h1>
        <button onClick={this.clickHandler}>Click Me</button>
      </div>
    );
  }
}

Upvotes: 0

Related Questions