Do we really need classes in JavaScript after all?
Dominik Lubański
Posted on December 11, 2018
Among a lot of other great features, ES2015 introduced the class syntax. For some, it was a missing piece in the object-oriented programming; for others something that we should have never added in the first place. Nevertheless, classes have become beloved by the library authors and users, so today, you can find them in almost every JavaScript library or framework.
Did classes deliver, what they promised? Three years later, I can say that besides simpler syntax (instead of using function constructors and prototypes), they failed in various fields. Let's explore together some of the foremost pitfalls.
classMyComponentextendsCoolComponent{constructor(one,two){// Use super() always before calling `this`// and don't forget to pass arguments 🤭super(one,two);this.foo='bar';}update(...args){this.value='...';// Does CoolComponent include update method or not? 🧐super.update(...args);}}
Class syntax might be confusing. Libraries usually force users to use extends keyword for consuming its API. As it might look straightforward, extending requires using super() calls wherever needed. To be sure, that our methods don't overwrite internal ones defined by the parent, we have to be careful how we name them (soon it will be possible to use a fancy # keyword to create private fields).
Super calls also can be tricky - for example, you can't use this in the constructor before calling super(). Oh, and don't forget to pass constructor arguments. You have to do it manually if you define constructor method.
Of course, we can get used to it. So we did. However, it doesn't mean that this is right.
classMyComponentextendsCoolComponent{constructor(){...// Change onClick method name and forget update it here 😆this.onClick=this.onClick.bind(this);}onClick(){this.foo='bar';}render(){return<buttononClick={this.onClick}>...</button>;}}
Classes are tightly bounded to this syntax. In class methods this represents an instance of the class. It was never intended to pass method definitions to another instance and lose that context. I know that library authors just wanted to squeeze out what's possible from the class syntax and at the same time be creative. Unfortunately, there is no one best solution for binding a function context. For the rescue, we will be able to use yet another new syntax - class fields, which simplifies creating methods pre-bounded to the instance.
classMyComponentextendsCoolComponent{// this method uses current state 🤨foo(){this.currentValue+=1;returnthis.currentValue;}// this method depends on other instance method 👆bar(nextValue){constvalue=this.foo();returnvalue+nextValue;}}classOtherComponentextendsMyComponent{// Ups, this.bar() is broken now 😡foo(){returnthis.otherValue;}}
Classes are hard to compose. The first problem here is with stateful methods. They can use the current state and return different results, even for the same input (passed arguments). The second factor is a well-known gorilla - banana problem. If you want to re-use class definition, you have to take it all or nothing. Even if you know what kind of methods parent includes, they might change in the future, so it is pretty easy to break something.
Moreover, it is almost impossible to take out a single method from the class definition, and re-use it in another one. Methods usually depend on each other or take values from class instance properties using this syntax. Yes, there is a mixins pattern, but it does not provide a clean and straightforward way for composing classes. If you wonder, there is a mixwith project for that and even ES proposal from the same author.
Is there any way out from those obstacles? Despite all of the classes burdens, they were for sure the best way to go forward in web development. The form of how we used plain objects before did not provide significant advantages over the classes. Because of that, library authors and users without thinking twice switched to them. So, is it possible to avoid all the classes problems and create a UI library, which is still powerful and easy to use at the same time?
For the last two years, I have been working on a library for creating Web Components, which I called hybrids. As the name suggests, it is a mix of two ideas - classes and plain objects. However, the final solution did not come to me just like that.
Initially, I followed common patterns, like other libraries. I built my API on top of the classes. Although, the primary goal of the library was to separate business logic from the custom element definition and let users avoid some of the classes problems (for example extends and super()). After a year, I almost finished my work, and I was ready to release a major version. The only last thing that bothered me a lot was a lack of composition mechanism. At the same time, I began to learn more about functional programming, and I liked it very much. I was sure then that class syntax was a blocker. I tried to study a lot about how to compose classes, but all the solutions were not sufficient in my opinion.
The breakthrough can only occur if you give up the available solutions and create new ones instead. For me, it was a mind-shift in how we can define components. All those problems have become an impulse to start the process again, but this time in a completely different way. Instead of using existing ideas, I started with an empty file where I tried to create a public API example, which solves those problems. Finally, I ended with something similar to this:
There is neither class nor this syntax, only simple values and pure functions in the definition inside of the plain object. Moreover, objects definitions can be composed with ease, as they are maps of independent properties. Custom define() function creates a class dynamically, applies properties definitions on the prototype and finally defines a custom element using Custom Elements API.
At first, I thought that it is impossible to implement API like this in the way, that it would scale and allow building complex components with more logic than a simple counting button has. Still, day after day I tried to create better ideas and solutions to make this possible.
The hard work paid off. In May 2018 I released a major version of the library. The code, which you can see above is a fully working example from the documentation! All of this was only possible because of a number of ideas used together, like property descriptors, factories, and property translation, as well as cache mechanism with change detection.
However, what about the opening question from the title? Do my ideas are the answer? Time will tell. For now, I would be happy to discuss this topic with you 💡.
Extraordinary JavaScript UI framework with unique declarative and functional architecture
An extraordinary JavaScript framework for creating client-side web applications, UI components libraries, or single web components with unique mixed declarative and functional architecture
Hybrids provides a complete set of features for building modern web applications:
Component Model based on plain objects and pure functions
Global State Management with external storages, offline caching, relations, and more
App-like Routing based on the graph structure of views
Layout Engine making UI layouts development much faster
Localization with automatic translation of the templates content
Hot Module Replacement support without any additional configuration
Documentation
The project documentation is available at the hybrids.js.org site.
Quick Look
Component Model
It's based on plain objects and pure functions1, still using the Web Components API under the hood:
Do you want to know more? In my upcoming posts, I will explain in detail all of the core concepts of the hybrids library. For now, I encourage you to look at the project homepage and official documentation.