Monday, 3 December, 2018 UTC


Summary

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!