Recently I have been learning React and I ran into something in JavaScript that I hadn't expected.
Here is an example of some code I was playing with. This code is a modified version of the code at https://reacttraining.com/react-router/web/example/auth-workflow.
class Login extends React.Component {
constructor() {
this.state = {
redirectToReferrer: false
}
}
login() {
fakeAuth.authenticate(() => {
//the problem is here
this.setState(() => ({
redirectToReferrer: true
}))
})
}
render() {
//...some additional logic here
return (
<div>
<p>You must log in to view the page</p>
<button onClick={this.login}>Log in</button>
</div>
)
}
}
I was rather shocked to find that when I clicked on the button, the browser complained that the setState
method did not exist!
It turns out that even with the class
syntax that debuted in ES2015, the methods of the class are not bound to a given instance. Somehow I had not realized that this was the case. It's the same old problem of this
depending on the calling context. If we want the code to work, we have to bind the method ourselves, e.g. like so:
class Login extends React.Component {
constructor() {
super()
this.login = this.login.bind(this);
//etc...
}
}
Now, the actual example that I was looking at online uses a syntax I was not familiar with, presumably to get around this very problem. It turns out that it's called Class properties transform. It's currently available with Babel using the stage-2 preset. Here's what the new syntax looks like:
class Login extends React.Component {
//class properties transform
state = {
redirectToReferrer: false
}
//class properties transform
login = () => {
fakeAuth.authenticate(() => {
this.setState(() => ({
redirectToReferrer: true
}))
})
}
render() {
//...some additional logic here
return (
<div>
<p>You must log in to view the page</p>
<button onClick={this.login}>Log in</button>
</div>
)
}
}
I don't know quite what to make of this syntax. I'm not a language or JavaScript expert, but it just doesn't look right to me.
If we replace class
with function
, it reminds me of something like this:
function Login() {
this.state = {
redirectToReferrer: false
}
this.login = () => {
fakeAuth.authenticate(() => {
this.setState(() => ({
redirectToReferrer: true
}))
})
}
}
If we create an instance using new Login()
, this.setState
will now work regardless of the calling context.
However, is using classes and adding this new transform syntax really worthwhile in that case? It's as though this new syntax is trying to bridge the gap between what can be done with the function
and class
syntax: We can't just write this.state = value
in a class
outside of the constructor, but now we can kind of do it after all with transform class properties. In that case, maybe it should just have been allowed in class
in the first place.
I also played around a bit to see how this new syntax deals with inheritance. If we have a normal method in a superclass and an arrow function with the same name in a subclass, a call to super
in the subclass' method actually works.
However, super
doesn't currently work if both the superclass and the subclass use the arrow syntax:
class BaseClass {
arrowFunction = () => {
console.log('BaseClass arrowFunction called')
}
}
class SubClass extends BaseClass {
arrowFunction = () => {
super.arrowFunction()
console.log('SubClass arrowFunction called')
}
}
const t = new SubClass()
t.arrowFunction()
When we transpile this code using Babel with 'env' and 'stage-2' presets, and try to run the resulting code in node, we get:
C:\dev\test.js:34
_get(SubClass.prototype.__proto__
|| Object.getPrototypeOf(SubClass.prototype), 'arrowFunction', _this).call(_this);
^
TypeError: Cannot read property 'call' of undefined
at SubClass._this.arrowFunction (C:\dev\test.js:34:96)
It appears that arrowFunction
is not getting resolved in the prototype chain. I don't know if this is the intended behaviour or a bug.
Stuff like this gets me frustrated with JavaScript. It kind of feels as though JavaScript is chasing its own tail, adding syntactic sugar on top of more syntactic sugar, and the end result is still confusing. I don't know what the internal considerations may be here, but it just seems that if JavaScript is to have a class
syntax, doing so in a way that's more orthogonal, that doesn't require adding new syntax all the time, would be nice.
Am I wrong to be frustrated with this syntax? I'm always open to different perspectives.
Top comments (10)
Classes are shortcuts to JS's prototypes and not really a new feature. The prototype is (roughly) a set of stuff that implicitly use the same
this
as a default context (that you can still override as usual).When you declare
value = () => {}
, you don't write in the prototype, you merely create a property that's an arrow function, and arrow functions have as a default context the one of their parent object at declaration time.If both your parent and child class use that syntax, you are not overriding it, you are overwriting it.
It's much easier to just use
() => this.value()
in your template and let the class syntax be what it is.Plus, you get less memory used; methods in she prototype are shared between all instances, not class members declared through
=
.My feeling is that it's strange the way classes have been implemented, which is, as you say, as kind of thin wrappers around functions. I don't understand the idea behind that approach. This properties transform thing now seems to be doubling down on that initial strangeness. I should probably do some additional reading to try to find out what the story is behind these decisions. Looking at it from the point of view of an "end user" programmer, it's hard for me to understand what the underlying logic is. If they want to implement the idea of a "class," why not do so with semantics that are more familiar? It would have the added benefit that the class properties syntax would presumably not be needed.
The story behind this is that the specification of the JavaScript language is closely tied to what browser vendors are willing to implement. For a feature to land in JavaScript, vendors must first provide native support for it (which is a long, error-prone and sometimes difficult process).
So the reasoning behind these thin wrappers and weird syntax is that they're easier and faster to implement.
That said, some things could have used more time and thought put into them (ex. auto-binding of class functions/methods), as once something has made it into the specification changing/removing it is sometimes impossible (vendors also strive for maximum backward-compatibility).
Thank you, that’s a really helpful perspective!
I think @elarcis mostly covered it. My humble advise is that
Babel
is shit andsource maps
are 💩. If you want to prototype some javascript, create an index.html put some javascript in it (yes any modern javascript, as bleeding edge as async await) and run it in your local browser.Coming back to your original question, Javascript classes are not a misstep or a badly designed API. The problem fellow devs encounter while learning React and ES2015 together, is that they fail to distinguish which part is Javascript and which part is the Reacts magic (JSX I am looking at you). Now this is not because the developer is incompetent or anything, it is because React and the ecosystem built around it is so damn seamless _(importing css ? importing png? import magic?)_that it feels part of the language. Enough of my rant....
Coming back to your original question, you need to further dig into javascript and the prototypal inheritance.
Surprising to some of the React pundits out in the world, this works!
And anyone who is familiar with how
this
works in javascript would clearly see why and how it works.The problem of binding
this
starts to occur when you separate the context.This is exactly why you need to bind some of your methods in React class, because they would be called out of context, essentially a different
this
.I strongly suggest this book You don't know JS, it really made me understand the whole
this
wizardry.Thanks for the book link!
Remove the arrow function syntax and it works
Hmmm, interesting. That syntax without parentheses or assignment looks really weird. I tried it, but I can't seem to compile this code at all. I am using the presets "env", "react", and "stage-2"...
are you using babel? that's really odd, because it's vanilla ES syntax...
I think maybe you intended to put parentheses after
arrowFunction
, i.e:In that case
arrowFunction
is using the normal class method syntax. If that's what you meant, then yeah, the normal syntax puts the methods of the class in the prototype chain whereas using arrow functions with the "class properties" syntax appears not to. In the latter case, each instance will have its own copy of any such functions...I tried running your code as-is in my browser and it didn't work. I also tried running it in node both as-is and transpiled with babel, and neither worked. Hopefully it isn't me having a stroke or something! :)