The Beginner’s Guide to ReactJS — Notes Part 6
This is the sixth part of my notes on egghead.io’s The Beginner’s Guide to ReactJS.
Use Class Components with React
This section will show how implicit this
bindings are lost when updating state in React and how to deal with it in different ways. The title of the video would be better named "Setting State and this
".
Implicit Binding and Losing It
The this
binding is determined by these rules in order of precedence:
- If
new
is called,this
is bound to the newly constructed object. - If
call
orapply
(orbind
) is called,this
is bound to the specified object. - If a context object owning the call is called,
this
is bound to the context object. this
by default isundefined
in strict mode or a global object otherwise.
We will focus on number 3, which is called implicit binding.
This is a vanilla JavaScript example of an implicit binding and the implicit binding being lost.
var name = "GLOBAL OBJECT NAME";function greet (name) {
return `Hi ${name}, my name is ${this.name}!`
}var bob = {
name: 'Bob',
greet: greet
}bob.greet('Jane'); // Hi Jane, my name is Bob!var greetFn = bob.greet; // this binding is lost!greetFn('Jane'); // // Hi Bob, my name is GLOBAL OBJECT NAME!
Although bob
doesn't "own" the function greet
, you could say it does at the time bob.greet
is called. In this situation, we say bob
is the context object and this
is "implicitly" bound to bob
.
The this
binding to bob
is lost when bob.greet
is assigned to the plain function greetFn
because greetFn
is a plain function that references greet
. The default binding is applied and GLOBAL OBJECT NAME
is logged to the console.
It’s important to note that even though it greetFn
looks like a reference to bob.greet
, it's actually just a reference to the function greet
.
Let’s look at something similar:
var bob = {
name: 'Bob',
greet(name) {
return `Hi ${name}, my name is ${this.name}`;
}
}bob.greet('Jane'); // "Hi Jane, my name is Bob"var greetFn = bob.greet;greetFn('Jane'); // "Hi Jane, my name is !"
Since there is no global variable name
, nothing is returned.
Now we can understand how something similar happens when updating state in React, and how to deal with it.
Losing this
Binding in Class Components
We’ll start by making a button that increments a number when it’s clicked.
class Counter extends React.Component {
constructor(...args) {
super(...args)
this.state = {count: 0}
}
render() {
return (
<button
onClick={() =>
this.setState(({count}) => ({
count: count + 1,
}))}
>
{this.state.count}
</button>
)
}
}
ReactDOM.render(
<Counter />,
document.getElementById('root'),
)
Focus on on the onClick
value. We have an arrow function that increments state with this.setState
.
While this code is completely functional, we could assign the this.setState
function to an event handler for more readability.
The edited code would look like this:
handleClick() {
this.setState(({count}) => ({
count: count + 1,
}))
}
render() {
return (
<button
onClick={this.handleClick}
>
{this.state.count}
</button>
)
}
Uh oh! It doesn’t work anymore. If we open the developer’s console, we get Uncaught TypeError: Cannot read property 'setState' of undefined
.
this
's binding in this.setState
to the object created by class Counter
was lost when we assigned it to handleClick()
. We are just assigning a reference to the setState
function to handleClick()
. handleClick()
is also just a plain function. Because classes in ES6 are always run in strict mode, this
defaults to undefined
. Notice the parallels from the example above in JavaScript.
Preserving this
Binding in Class Components
We’ll cover several ways to ensure the this
binding is not lost.
One way uses “explicit binding,” or rule number 2 from the above this
binding rules. This is when we explicitly say what we want this
to bind to with call
, apply
, or bind
.
Here we’ll use the built-in bind
utility in the onClick
assignment. bind
returns a new function that is hard-coded to call the original function with the this
context object you specified. This variation of explicit binding involving assigning call
, apply
, or bind
expressions to functions is called "hard binding."
The timer will function properly if we edit onClick
like this:
onClick={this.handleClick.bind(this)}
While this works, this could be a performance bottleneck in some situations.
To deal with the bottleneck we can reference the prototypal method handleClick()
by adding this.handleClick
in the constructor and assigning it a pre-bound handleClick
method.
The constructor would look like this:
constructor(...args) {
super(...args)
this.state = {count: 0}
this.handleClick = this.handleClick.bind(this)
}
This is the same as the common pattern of lexically capturing this
in pre-ES6 code, like in var self = this;
.
This solution can be cumbersome if we need to do this for a lot of methods.
Let’s look at another solution with public class fields. In a nutshell, these let us declare class properties without the constructor.
Look at the code below for before and after public class fields are implemented.
Before:
constructor(...args) {
super(...args)
this.state = {count: 0}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState(({count}) => ({
count: count + 1,
}))
}
After:
state = {count: 0}
handleClick = this.handleClick.bind(this)
handleClick() {
this.setState(({count}) => ({
count: count + 1,
}))
}
We’ve moved this.state
and the this.handeClick
assignments out of the constructor and into the class body. this.
is no longer necessary since state
and handleClick
are in the class body. state
and handeClick
are the public class fields and we assign expressions to them. Since the constructor has become the same as the default constructor, we've removed it.
The incrementing counter will work with this implementation.
We could refactor this by removing the handleClick()
method from the prototype and having it only on the instance.
state = {count: 0}
handleClick = function() {
this.setState(({count}) => ({
count: count + 1,
}))
}.bind(this)
While this works, we have to call the .bind
method every time we update state. We can use the lexical this
that are possible with arrow functions to workaround this.
handleClick = () => {
this.setState(({count}) => ({
count: count + 1,
}))
}
Which way is best?
We discussed four main ways:
- Hard binding the method with
.bind
in event handler assignment value - Lexically capturing the prototype method in the constructor
- A variation of number 2 using public class fields
- Using lexical
this
via arrow functions
The issue with number 1 is that it can be a performance bottleneck in some situations. Number 2 can be cumbersome if we have to do it for many methods. Number 3 is currently an experimental implementation; it’s probably not good for production code (although it’s commonly used in many projects).
Number 4 works and is very common, but it’s not without its criticisms. YDKJS author Kyle Simpson says that using lexical this
reinforces a bad practice of evading a solid understanding of this
. He suggests people either stick with lexical style code or embrace this
and avoid lexical this
.
Either way it’s important to understand both hard binding and lexical this
so you can understand others' code and adapt to any situation when working in a team.
TL;DR
Be careful of losing the this
binding when updating state in class components. You can keep the binding with either the .bind
method, capturing lexical this
via assignment in the constructor or with public class fields, or with the lexical this
included in arrow functions.
Manipulate the DOM with React refs
This section will explain how to manipulate the DOM node directly with React’s ref
prop. Sometimes this is necessary for some JavaScript libraries to work or when we want to get the value of form fields.
We’ll use vanilla-tilt.js as an example on how to make a JavaScript library functional with ref
.
Making a Static Image
This is our base code:
class Tilt extends React.Component {
render() {
return (
<div className="tilt-root">
<div className="tilt-child">
<div {...this.props} />
</div>
</div>
)
}
}const element = (
<div className="totally-centered">
<Tilt>
<div className="totally-centered">
vanilla-tilt.js
</div>
</Tilt>
</div>
)ReactDOM.render(
element,
document.getElementById('root'),
)
Class Tilt
renders a div with the class tilt-root
, which nests a div with the class tilt-child
, which nests a div that spreads the props. tilt-root
styles one div to have the colored gradient while tilt-child
is a smaller white box. Both also have styles for animations we'll see later.
We then create constant element
, which is the Tilt
component wrapped in a div with the class totally-centered
. There is another div with the same class nested inside of Tilt
with the words "vanilla-tilt.js". totally-centered
are flexbox styles that horizontally and vertically center content.
This code produces this static image:
Adding ref
Taken from the React docs,
The ref attribute takes a callback function, and the callback will be executed immediately after the component is mounted or unmounted.
When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument.
In our code, we’ll use the ref
callback to store a reference to our desired DOM node.
The render method now returns this:
return (
<div
ref={node => (this.rootNode = node)}
className="tilt-root">
<div className="tilt-child">
<div {...this.props} />
</div>
</div>
It’s a good practice not to assign arrow functions to refs since arrow function assignments are recreated on each rerender and for other reasons.
We could rewrite the code like this:
setRef = node => {
return this.rootNode = node
}
render() {
return (
<div
ref={this.setRef}
We can see we’re accessing the node we want with:
componentDidMount() {
console.log(this.rootNode)
}
Using the Node with a Library
Now we can use this.rootNode
with the vanilla-tilt library.
We bring in the vanilla-tilt library with a script <script src="https://unpkg.com/vanilla-tilt@1.4.1/dist/vanilla-tilt.min.js"></script>
.
Then we can use the vanilla-tilt library by putting the VanillaTilt
global in the ComponentDidMount
method because we can access the node after the component mounts. We initialize VanillaTilt
with init
then pass in some options onto the node (this.rootNode
). Our new code looks like this:
componentDidMount() {
VanillaTilt.init(this.rootNode,{
max: 25,
speed: 400,
glare: true,
'max-glare': 0.5,
})
}
It works!
TL;DR
To manipulate the DOM,
- Pass on a
ref
on the element that you're rendering. Puttingref
on a class references an instance of that class. - In the
ref
value, pass in the node as the argument and return an assignment of the node to a value in the instance (ex:this.rootNode
). - After the component is mounted, the node can be used for a library or something else.
Edit: Thanks to Eddy Wilson for the tip on declaring refs in the class body!