Before getting to React, it's helpful to know what this
does generally in Javascript.
Take the following snippet of code. It's written in ES6 but the principles for this
predate ES6.
class Dog {
constructor() {
this.favoriteWord = "Woof!";
}
bark() {
return this.favoriteWord;
}
}
let dog = new Dog();
dog.bark(); // => Woof!
Cool. That makes sense. But remember, functions are also objects in their own right in Javascript, so let's try this:
let bark = dog.bark;
bark(); // => Error
Ruh roh. What happened? You probably got a message saying favoriteWord
wasn't
a property of undefined
, or something similar. But this.favoriteWord
clearly
refers to the Dog
instance, so what gives?
The answer is that this
is determined at the time the function is called,
not the time the function is defined. This can be super confusing if you're
coming from, say, Python, where the following code works:
class Dog:
def __init__(self):
self.favorite_word = "Woof!"
def bark(self):
return self.favorite_word;
dog = Dog()
bark = dog.bark
bark() # => Woof!
It's tempting to say this
and self
do the same things. They both refer
to the parent of a function in this case. But in the case of Python, self
is determined at the time the function is defined. In the case of Javascript,
this
is determined at the time the function is called.
The bark
variable is distinct from dog.bark
.
To make this a little clearer, let's just use a different name.
let bark2 = dog.bark;
bark2();
We're creating a new variable (bark2
). bark2
doesn't have a parent at the
time it's called, unlike dog.bark
(which has dog
as a parent). bark2
is a top level variable, so it's "parent" is undefined
. undefined
doesn't
have a favoriteWord
property, so calling the function results in an exception.
Before a Javascript expert shouts at me, note that in Javascript-land, we
generally think of the relationship between dog
and bark
not as "parent"
and "child", but that dog
is the "context" for bark
. dog.bark
means
"Call the bark function with the context of dog". And this
always refers
to the context in a given function.
To understand why someone (if not necessarily you or me) might think this is cool, consider the following:
let cat = {
favoriteWord: "Meow!"
};
cat.meow = bark;
cat.meow(); // => "Meow!"
bark(); // Nope, still broken
This works because the this
variable in the bark
function isn't tied to the
original dog
object. So we can freely assign bark
to cat.meow
. And when
we call cat.meow
, the caller is cat
, so this.favoriteWord
refers to "Meow!"
instead of "Woof!".
As an aside, the "context" for a Javascript function or class method is distinct
from "context" in React.
As the React developers themselves indicate, if you're just getting started with
React, ignore React's version of context. However, you do need to understand
context in the Javascript sense insofar that you're using classes to represent
React components and invoking stuff like this.props
or this.setState({ ... })
.
OK, let's add a new wrinkle. Consider this now:
let alwaysWoof = bark.bind(dog);
alwaysWoof(); // => "Woof!"
Why does this work? It's because calling bind
on a function returns a copy of
that function in which this
is always set to whatever arg you pass to bind
.
This applies even if we change the caller of the bound function:
cat.meow = alwaysWoof;
cat.meow(); // => "Woof!"
In the class context, it's pretty common to bind to this
:
class ConsistentDog {
constructor() {
this.favoriteWord = "Woof!";
let bark = function() {
return this.favoriteWord;
}
this.bark = bark.bind(this);
}
}
let conDog = new ConsistentDog();
let conBark = conDog.bark;
conBark(); // => "Woof!"
Writing .bind(this)
over and over is pretty annoying, so in ES6,
you can also avoid writing .bind(this)
with the () => ...
syntax:
class SimpleDog {
constructor() {
this.favoriteWord = "Woof!";
this.bark = () => this.favoriteWord;
/*
Or you can do this if you need more than one statement
for your function.
this.bark = () => {
let simpleWord = this.simpleWord;
return simpleWord;
};
*/
}
}
let simDog = new SimpleDog();
let simBark = simDog.bark;
simBark(); // => "Woof!"
Still with us? OK, now to bring in React. Consider this React component, defined as an ES6 class:
class Welcome extends React.Component {
render() {
return <button onClick={this.sayName}>Say My Name</button>;
}
sayName() {
alert(this.props.name);
}
}
In React, you invoke like this: <Welcome name="Bob" />
. This renders a button.
Clicking the button should trigger an alert with "Bob".
Except it doesn't. Because in the above example, this
would be undefined in the
sayName
function.
What's happening inside the render function is that this
refers to the current instance
of our React component. That component has a sayName
function defined, so this.sayName
points to our function, just fine and dandy.
But what React is doing behind the scenes is assigning this.sayName
to another variable.
That is, it's just like this:
let onClick = this.sayName;
onClick(); // Technically a click event is passed to onClick
// but this doesn't matter for our purposes
And just like our dog example, we get an error. Because this
is undefined. This is
extra confusing because in previous versions of React, React would "autobind" the
event handler for you, so it would work. But at some point, Facebook decided to
stop doing that, so ... here we are.
So how can we fix our component? We just do binding ourselves, like this:
<button onClick={this.sayName.bind(this)}>Say My Name</button>;
Or with ES6 syntax:
<button onClick={() => this.sayName()}>Say My Name</button>;
And it should work!
One final note -- when we bind a function in React, we can do that not only when the render function is called, but before as well. So take this:
class Welcome extends React.Component {
constructor(props) {
super(props);
this.boundSayName = this.sayName.bind(this);
}
render() {
return <button onClick={this.boundSayName}>Say My Name</button>;
}
sayName() {
alert(this.props.name);
}
}
We can do this.boundSayName
instead of this.boundSayName.bind(this)
. Because
this.boundSayName
was already bound to this
in the constructor.
And that's it! Hope it helps!