DEV Community

Cover image for JavaScript Frustration: Classes and Class Properties Transform
Nested Software
Nested Software

Posted on • Edited on • Originally published at nestedsoftware.com

JavaScript Frustration: Classes and Class Properties Transform

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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...
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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
      }))
    })
  } 
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
elarcis profile image
Elarcis • Edited

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 =.

Collapse
 
nestedsoftware profile image
Nested Software • Edited

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.

Collapse
 
klintmane profile image
Klint

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).

Thread Thread
 
nestedsoftware profile image
Nested Software

Thank you, that’s a really helpful perspective!

Collapse
 
kepta profile image
Kushan Joshi • Edited

I think @elarcis mostly covered it. My humble advise is that Babel is shit and source 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.


class Foo {
    method() {
        console.log(this.speak());
    }
    speak() {
        console.log('I am a class');
    }
}
z = new Foo();
z.method(); // I am a class

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.

setTimeout(z.method, 2000);
// after 2 secs
// Uncaught Error: Cannot read property 'speak' of undefined

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.

Collapse
 
nestedsoftware profile image
Nested Software • Edited

Thanks for the book link!

Collapse
 
nektro profile image
Meghan (she/her) • Edited

Remove the arrow function syntax and it works

class BaseClass {
    arrowFunction {
      console.log('BaseClass arrowFunction called')
    }
}

class SubClass extends BaseClass {
    constructor() {
        super();
    }
    arrowFunction {
        super.arrowFunction()
        console.log('SubClass arrowFunction called')
    }
}

const t = new SubClass()
t.arrowFunction()
Collapse
 
nestedsoftware profile image
Nested Software

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"...

Collapse
 
nektro profile image
Meghan (she/her)

are you using babel? that's really odd, because it's vanilla ES syntax...

Thread Thread
 
nestedsoftware profile image
Nested Software • Edited

I think maybe you intended to put parentheses after arrowFunction, i.e:

  arrowFunction() { // <--- added parentheses
    console.log('BaseClass arrowFunction called')
  }

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! :)