react christmas

Why is this so hard?

←Previous postNext post →

I've lost count of the number of times I forgot to bind this. Why do we need to bind this and how should we do it?

A 4 min read written by
Svein Petter Gjøby

Why is this so hard?

To better understand why we need to bind this in React, we should understand how this works in JavaScript. Let's take a shot at it with the following snippet.

class Santa() {
  constructor() {
    this.catchphrase = "Ho ho ho ho!";
  }

  greet() {
    return this.catchphrase;
  }
}

const santa = new Santa();
console.log(santa.greet()); // Ho ho ho ho!

Awesome! That worked as expected, but will this?

const santa = new Santa();
const greet = santa.greet;
console.log(greet()); // Uncaught TypeError: Cannot read property 'catchphrase' of undefined

Wait, what!? We got a TypeError saying catchphrase isn't a property of undefined. Why is that?

In JavaScript, this is determined when a function is called, not when the function is defined. Moreover, this refers to the context of the function.

In the above example we create a new variable greet. greet is a top level variable so its context is undefined, and undefined does not have a property named catchphrase. Unlike santa.greet which has santa as its context, due to implicit binding.

To better understand implicit binding, consider this example.

const santa = new Santa();
const greet = santa.greet;

const sheldon {
  catchphrase: "Bazinga!"
}

sheldon.greet = greet;
console.log(sheldon.greet()); // Bazinga!

Bazinga! This works because the greet function is called in the context of sheldon, which has a property named catchphrase.

Binding it ourselves

Sometimes we might want to specify the context of a function. This can be achieved with the bind function, and is called explicit binding.

const santa = new Santa();
const badGreet = santa.greet;

const goodGreet = badGreet.bind(santa);
console.log(goodGreet()); // Ho ho ho ho!
console.log(badGreet()); // Uncaught TypeError: Cannot read property 'catchphrase' of undefined

Calling bind on a function returns a copy of the funciton with the argument as the context. In other words this is always whatever argument you pass to bind for that function. This even works if the implicit context is changed.

sheldon.greet = goodGreet;
console.log(sheldon.greet()); // Ho ho ho ho!

What about React?

Let's transform our example into a React component.

class Santa extends React.Component {
  constructor(props){
    super(props);

    this.state = {
      catchphrase: "Ho ho ho ho!"
    }
  }

  render() {
    return (
      <button 
        onClick={this.sayCatchphrase}
      >
        Say catchphrase
      </button>
    );
  }

  sayCatchphrase() {
    console.log(this.state.catchphrase);
  }
}

Clicking the button should ideally print Ho ho ho ho! to the console. However, it does not in the above example.

In the sayCatchphrase function this would be undefined. This is because the event handler function loses its implicitl binding. As mentioned previously, the context is determined when the function is called. When the event occurs and the function is invoked, the context falls back to the default binding and is set to undefined.

To fix this we have a couple of options. We could bind the function ourselves:

<button 
  onClick={this.sayCatchphrase.bind(this)}
>
  Say catchphrase
</button>

It is considered best practice to avoid binding your functions in the render method.

We can use an arrow function. This works because this is bound lexically. This means that it uses the context of the render method as its context.

<button 
  onClick={() => this.sayCatchphrase()}
>
  Say catchphrase
</button>

Or we can use the public class field syntax, which is still experimental and a babel plugin is required to use it.

class Santa extends React.Component {
  constructor(props){
    super(props);

    this.state = {
      catchphrase: "Ho ho ho ho!"
    }
  }

  render() {
    return (
      <button 
        onClick={this.sayCatchphrase}
      >
        Say catchphrase
      </button>
    );
  }

  sayCatchphrase = () => {
    console.log(this.state.catchphrase);
  }
}

That's it!

←Previous postNext post →