Mjuice
Mjuice

Reputation: 1288

React Component's onClick handler not binding to "this"

I have a react component in which I am trying to pass a span tag an onClick event handler. The event handler is called "askQuestion". I bind the onClick event handler to the context of the component with the .bind(this, parameter) function.

Despite my attempt to bind to "this" I'm still getting an error in the dev tools console saying "cannot read property askQuestion of undefined." I'm pretty sure this error means that askQuestion is not bound properly to the context of the component. I need help binding askQuestion properly.

Here is the component:

class Questions extends Component {
  askQuestion(question) {
    alert("hello");
  }

  addQuestion(question, index) {
    return (
      <div key={index} className="col-xs-12 col-sm-6 col-md-3">
        <span onClick={this.askQuestion.bind(this, question)}>
          {question.q}
        </span>
      </div>
    );
  }
  render() {
    return (
      <div id="questions" className="row">
        <h2>Questions</h2>
        {this.props.questions.map(this.addQuestion)}
      </div>
    );
  }

}

Upvotes: 1

Views: 931

Answers (2)

Andrew Li
Andrew Li

Reputation: 57944

Explanation

The problem is that you use Array.prototype.map, which does not bind this unless explicitly told to, and the context of the callback is, in turn, undefined. From the documentation:

If a thisArg parameter is provided to map, it will be passed to callback when invoked, for use as its this value. Otherwise, the value undefined will be passed for use as its this value. (emphasis mine)

Where thisArg is an optional argument of map, see Array.prototype.map Syntax. That means, when you do:

{this.props.questions.map(this.addQuestion)}

this context is undefined when calling this.addQuestion, thus it is undefined in the call to addQuestion. Let me illustrate the problem further by taking a look at your addQuestion method:

addQuestion(question, index) {
  return (
    <div key={index} className="col-xs-12 col-sm-6 col-md-3">
      <span onClick={this.askQuestion.bind(this, question)}>
        {question.q}
      </span>
    </div>
  );
}

Here, since, as mentioned earlier, this is undefined, you are actually trying to do:

undefined.addQuestion.bind(undefined, question)

which throws the error because undefined has no function addQuestion.


Solution

Again, from the documentation:

Syntax

var new_array = arr.map(callback[, thisArg])

thisArg

Optional. Value to use as this when executing callback.

You can see that we can explicitly pass a this context to map, to be used as this context in the callback. That means we can pass an additional argument to be this in the function. This can be applied like so:

{this.props.questions.map(this.addQuestion, this)}

Since this refers to the actual component here, the component is passed as this. That will then correctly call the method addQuestion from the component. An alternative and functionally equivalent way to do is like so:

{this.props.questions.map(this.addQuestion.bind(this))}

Again, since this refers to the actual component here, the component is bound as the this context of the method. That means, in the call to addQuestion, the component is this, like the above solution.


Another thing I'd recommend is that, instead of creating a new function every time with bind, do it once in the constructor:

constructor(/* props if needed */) {
    /* super(props) if needed */
    this.addQuestion = this.addQuestion.bind(this);
}

That way, the bound method is already this.addQuestion. You can then get rid of all your bind(this)s, but keep your bind(this, question), as you bind with an argument.

Upvotes: 1

Huy
Huy

Reputation: 11206

I believe the proper way to do this in React is to bind this in the constructor.

constructor(props) {
  super(props)
  this.askQuestion = this.askQuestion.bind(this)
}

Upvotes: 2

Related Questions