The Buttons Aren't Alright

by Alec Barlow

Ever had the bug where event handlers tied to your React components don't fire? It happens to all of us, don't worry.

The best way to learn is by doing, as my alma mater says. In that spirit, there are three embedded CodePens below that you can edit, fix, or play around with. Each one contains a React component with a bug in the button's on-click event. The left side of each Pen contains the source code. The right side contains two parts: the top section is the output from the React component, while the bottom section has a couple of tests that will run automatically to make sure the component works correctly. If all tests pass, you probably figured out the problem (or changed/cheated the tests).

Problem #1

See the Pen zwWReJ by Alec Barlow (@abarlow85) on CodePen.

Solution #1

First, take a look at the AssertionError that our test is producing.

TypeError: Cannot read property 'state' of undefined

Let's trace our code flow. When the button is clicked, the onClick handler is called, which is this.increment. Looking at the code for that method, the only time we are referring to state is on line 10, when using destructuring get the value of clicks. So this error is occuring because we are trying to read the property state from something that is undefined. In essence, this.state is being interpreted as undefined.state.

How can it be? Shouldn't this be referring to our Button component? In this case, no; React is not causing the issue, JavaScript is. According to React's documentation, "class methods are not bound by default", meaning that if you refer to a class method without invoking it using (), it needs to be bound with a context. Without a context, this will remain undefined.

And finally, a solution. Please be aware that are multiple ways to bind a method, which I will go over in the next sections.

  
    <button onClick={this.increment.bind(this)}>Click Me!</button>
    // Note: This solution has a very small,
    // negative effect on performance.
    // Follow along to discover why.
  

Problem #2: Don't use the solution in problem 1

See the Pen Lydgvv by Alec Barlow (@abarlow85) on CodePen.

This time, the AssertionError is different. The component does not raise an error, but the button is not incrementing the number of clicks. So it looks like our onClick handler is not actually firing.

Take a look at the expression inside the handler. We are wrapping a callback function around this.increment. But wait, is this.increment actually being invoked? Of course not! It's missing ()!

The solution:

  
    <button onClick={() => this.increment()}>Click Me!</button>
    // Note: This solution also has a very small,
    // negative effect on performance.
    // Follow along to discover why.
  

Did you notice that we did not need to use .bind(this) anywhere. As mentioned in the explanation of the previous problem, we only need to bind the context when referring to a method without invoking it. Since we are invoking the method with parentheses, its context is bound, and this will not be undefined.

Problem #3: Don't use previous solutions

See the Pen zwWMwX by Alec Barlow (@abarlow85) on CodePen.

Obviously, something is wrong. Hopefully, the problem isn't me boring you, either. In all seriousness, our component has a big problem. Once again, take look at the onClick handler.

onClick={this.increment()}

The method is being invoked, and we see that the number of clicks increased, meaning its context is bound. The reason this is causing an infinite loop has to do with a key feature of React:

By default, a change in a component's state causes it to re-render.

To understand what is happening, lets list out the sequence of events:

  1. Component renders
  2. onClick fires during render because we are invoking it
  3. this.increment() causes a change in state because clicks increase
  4. Component re-renders due to a change in state
  5. onClick fires during render because we are invoking it
  6. this.increment() causes a change in state because clicks increase
  7. Component re-renders due to a change in state

You get the picture...

The purpose of event handlers is to only fire when the event occurs. An on-click event isn't supposed to fire when our component renders, and doing so creates an infinite loop. A refactor is required. For the solution, I do not want to bind context inside the render method, or wrap a callback around this.increment. This time, let's do the binding in the constructor method.


    constructor(props) {
      super(props)
      this.state = {
        clicks: 0
      };
      
      this.increment = this.increment.bind(this)
    }
    
    ...
    
    render() {
      ...
      
      return (
        ...
        <button onClick={this.increment}>Click Me!</button>
        // No () after this.increment because we
        // do not want to invoke the handler on render!
      );
    }
  

Method Binding: When, Where, and How

You have seen three different ways of binding context to an on-click event handler. To summarize:

So what method should you use? From a performance standpoint, method 1 and 2 are not as efficient as method 3. Each time the component renders, the event hander is being re-bound in method 1, while in method 2, a new callback function is being created. In method 3, the binding occurs once, and only once, when the component is initialized. You can also find a fourth method with a Google search, but it uses experimental syntax, which may or may not be supported in the future.

In sum, method 3, while requiring more redundancy in your code, is the method recommended by Facebook. But for smaller apps, it probably doesn't matter, and if you are just learning React, I recommend trying to understand that there are several ways of doing the same thing.

Previous Post Next Post