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!