Do we really need classes in JavaScript after all?

smalluban

Dominik Lubański

Posted on December 11, 2018

Do we really need classes in JavaScript after all?

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.

class MyComponent extends CoolComponent {
  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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

class MyComponent extends CoolComponent {
  constructor() {
    ...
    // Change onClick method name and forget update it here 😆
    this.onClick = this.onClick.bind(this); 
  }

  onClick() {
    this.foo = 'bar';
  }

  render() {
    return <button onClick={this.onClick}>...</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

class MyComponent extends CoolComponent {
  // this method uses current state 🤨
  foo() {
    this.currentValue += 1;
    return this.currentValue;
  }

  // this method depends on other instance method 👆
  bar(nextValue) {
    const value = this.foo();
    return value + nextValue;
  }
}

class OtherComponent extends MyComponent {
  // Ups, this.bar() is broken now 😡
  foo() {
    return this.otherValue; 
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

import { html, define } from 'hybrids';

function increaseCount(host) {
  host.count += 1;
}

const SimpleCounter = {
  count: 0,
  render: ({ count }) => html`
    <button onclick="${increaseCount}">
      Count: ${count}
    </button>
  `,
};

define('simple-counter', SimpleCounter);
Enter fullscreen mode Exit fullscreen mode

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

GitHub logo hybridsjs / hybrids

Extraordinary JavaScript UI framework with unique declarative and functional architecture

hybrids

build status coverage status npm version

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:

import { html, define } from "hybrids";
function increaseCount(host) {
  host.count += 1;
}

export default define({
  tag: 
Enter fullscreen mode Exit fullscreen mode

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.

You can also watch my Taste the Future with Functional Web Components talk, which I gave at the ConFrontJS conference in October 2018, where I explained how I came to those ideas.


🙏 How can you support the project? Give the GitHub repository a ⭐️, comment below ⬇️ and spread the news about hybrids to the world 📢!


👋 Welcome dev.to community! My name is Dominik, and this is my very first blog post ever written - any kind of feedback is welcome ❤️.

Cover photo by Zach Lucero on Unsplash

💖 💪 🙅 🚩
smalluban
Dominik Lubański

Posted on December 11, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related