React and the mysteries of this
What’s going on with this
in JavaScript and what does bind()
do? Let’s explore how this
behaves in JavaScript and learn why the React documentation recommends calling bind()
on some methods in your class component constructors.
When this
confuses you
Let’s begin with an example.
Your component displays a button which should update the text in the <output>
element when clicked.
class MyAwesomeButton extends React.Component {
constructor() {
super();
this.state = { message: 'Not clicked yet' };
}
showConfirmation() {
this.setState({ message: 'Clicked!' });
}
render() {
return (
<form>
<output>{this.state.message}</output>
<button onClick={this.showConfirmation}>Click Me</button>
</form>
);
}
}
You can run this example in this pen.
constructor()
sets up the initial message by assigning { message: 'Not clicked yet' }
to this.state
. The render()
method places this.state.message
in an <output>
HTML element.
showConfirmation()
updates this.state.message
by calling setState()
.
To invoke showConfirmation()
as an event handler when the user clicks the button, we pass this.showConfirmation
as the onClick
prop to the <button>
element,
But when you click the button, nothing happens. Instead, an ominous message appears in the browser console:
Error: this is undefined
Uh? In other languages, this
lets you always access instance fields like state
. But while calling showConfirmation()
with this
works, accessing this.state
doesn’t. Why?
How this
works
Let’s study a JavaScript function in isolation:
function sayName() {
return this.firstName;
}
It’s legal to use this
in any function, so we can define sayName()
outside of a class. You can run the code in this pen. What’s going to happen if we call sayName()
?
sayName(); // error firstName is not defined
Outside modules and classes, this
is the window
object and sayName()
prints the value of window.firstName
. Since window.firstName
does not exist, we’re getting an error, but you can change that if you assign a string to window.firstName
.
window.firstName = 'George';
sayName()
returns 'George'
.
Now sayName()
prints the value that you assigned to window.firstName
.
If you call sayName()
differently, you can also change the value of this
. For example, you can make sayName()
output 'Henry'
by first defining an object where the firstName
property points to the 'Henry'
string:
const henry = { firstName: "Henry" };
then assigning sayName()
to a field on the henry
object:
henry.talk = sayName;
When you invoke talk()
on henry
, this
points to henry
, so it returns 'Henry'
, even though talk()
is the same function as sayName()
.
henry.talk(); // Henry
If you call a function through an object field, this
points to the object.
Methods defined inside a class behave similarly to functions defined on the object instance. For example, create a Henry
class and, in the constructor, assign the value 'Henry'
to this.firstName
.
class Henry {
constructor() {
this.firstName = 'Henry';
}
talk() {
return this.firstName;
}
}
When you instantiate the Henry
class, JavaScript automatically creates a firstName
property on the henry2
object with value 'Henry'
.
const henry2 = new Henry();
Calling henry.talk()
then automatically references the firstName
property on the henry
object.
henry2.talk(); // returns 'Henry'
When you use React class components, it’s as if React created an instance of our component classes and called the class methods on the object instance.
But if you define a method inside a class and you don’t call it on an object directly, then this
is still undefined
.
Let’s try it with our talk()
method on the Henry
class. First define a function that calls another function:
function callOther(otherFunction) {
otherFunction.call();
}
In JavaScript, functions are also objects that you can pass around and call with the call()
method. Try passing henry2.talk()
to callOther()
:
/* error: this is undefined */
callOther(henry2.talk);
Even though you’ve defined talk()
on the henry
object, when callOther()
invokes talk()
, it does not use the henry2
object, so this
doesn’t point to henry2
. On top of that, you defined talk()
as a class method, so talk()
runs in strict mode: therefore this
ends up undefined
.
You can even use call()
to make this
inside a function point to an arbitrary object.
// returns 'George'
sayName.call({ firstName: 'George' });
The code that calls the function determines the value of this
, even if you originally defined the function in an object or class. Since React does not take care of binding this
to the component instance inside an event handler, this
ends up undefined
.
What if you want this
inside event handlers to always point to the component instance?
Solve this
!
When you need to update a component state, you need a reference to the component instance. Unfortunately, by the time React calls the event handler, this
doesn’t point to the component instance anymore. The code inside modules and classes runs in strict mode, which prevents this
from defaulting to the window
object, so this
ends up undefined.
There are three ways you can force this
to point forever to the value that this
had when you defined the function, so you’ll be able to access this.state
in event handlers.
The first solution is the bind()
method. Calling bind()
on a function makes this
inside the function forever point to the bind()
argument. If we call bind()
on the sayName()
function with henry
as an argument, bind()
returns a new function, where this
always points to henry
.
const boundSayName = sayNameBind(henry);
// returns 'Henry'
boundSayName();
// still returns 'Henry'
boundSayName.call({ firstName: 'Tamara' });
bind()
can fix our button component. Call bind()
on the this.showConfirmation
in the constructor:
constructor() {
super();
this.state = { message: '' };
this.showConfirmation = this.showConfirmation.bind(this);
}
bind()
creates a new function where this
forever points to the value of this
in the constructor. Since this
in the constructor points to the component instance, this
inside showConfirmation()
will also always point to the component instance. Notice that when you assign the return value of bind()
to this.showConfirmation
, you replace the original showConfirmation()
with the new function.
Another solution is replacing methods with arrow functions. Arrow functions, defined with an arrow (=>
) instead of the function
keyword, always keep the value of this
to whatever it was at the place where you defined the function. For example, let’s create a top-level sayNameArrow()
function in the browser console:
const sayNameArrow = () => this.firstName;
Since an arrow function preserves the value this
had at the moment the function was defined, and this
points to window
when we define sayNameArrow
, even if we assign sayNameArrow()
to an object field, it always returns window.firstName
:
henry.talkArrow = sayNameArrow;
// returns window.firstName
henry.talkArrow();
// also returns window.firstName
talkArrow.call({ firstName: 'George' });
Instead of using methods, we can define the event handlers as arrow functions in the constructor:
class MyAwesomeButton extends React.Component {
constructor() {
super();
this.state = { message: 'Not clicked yet' };
this.showConfirmation = () => {
this.setState({ message: 'Clicked!' });
};
}
The last solution, class properties, has not yet reached the JavaScript standard, but it’s already a candidate recommendation at the time of writing. Class properties let you create any fields by placing their name followed by a colon in the class body:
class MyAwesomeButton {
showConfirmation: () => {
this.setState({ message: 'Clicked!' });
}
}
This is the same as creating an arrow function in the constructor, but it makes the code easier to parse as you don’t need to look at the code in the constructor to understand what’s going on.
If you use Babel, you can enable support for class properties. Class properties are already enabled if you use create-react-app to manage your application.
All three solutions create a function on the component instance, where this
always points to the component instance itself. It would be interesting if you could access the component state without the need to access the instance, maybe with an argument that React would pass to all event handlers, but that’s not possible with the current React version, so we need these workarounds to fix this
.
If you want to learn more about this
, check Kyle Simpson’s book and if you want to learn more about React, check out my book React for Real.