From classes to plain objects and pure functions
Tags: webdev, webcomponents, javascript, functional
Published: 2019-01-10 19:00:37

---
title: From classes to plain objects and pure functions
published: true
description: The hybrids library uses plain objects instead of classes. Let's break down the core concepts in the step by step process and find out how it is possible.
tags: webdev, webcomponents, javascript, functional
series: Core Concepts of the Hybrids Library
cover_image: https://thepracticaldev.s3.amazonaws.com/i/iwg0z2y0mxujs8eyjwbl.jpg
---

*This is the first in a series of posts about core concepts of [hybrids](https://github.com/hybridsjs/hybrids) - a library for creating Web Components with simple and functional API.*

ES2015 has introduced classes that are now widely used in UI libraries and frameworks. However, are they the best way for creating component-based logic in JavaScript? In my last post, I've highlighted some of the main classes pitfalls:

{% link smalluban/do-we-really-need-classes-in-javascript-after-all-91n %}

The hybrids library is the result of research on how we can take a different approach, and create simple and functional tools for building web components. However, the only way to create a custom element is to use a `class`, which extends `HTMLElement`, and define it with Custom Elements API. There is just no other way (you can also use function constructor with properly reflected `super()` call). So, how is it possible that hybrids uses plain objects instead of classes?

The answer is a combination of three property-related concepts used together: property descriptors, property factories, and property translation. Let's break down those concepts into the step by step process with a simple custom element definition.

## Step 1: Use Custom Elements API

For a better understanding of the process, we are going to use an example with minimal requirements of the Custom Elements API. The goal here is to show how we can switch from class definition to plain object with pure functions.

```javascript
class MyElement extends HTMLElement {
  constructor() {
    this.firstName = 'Dominik';
    this.lastName = 'Lubański';
  }
  
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

customElements.define('my-element', MyElement);
```

Our custom element definition has two simple properties (`firstName` and `lastName`) and one computed property, which returns the concatenation of the first two. The example does not contain methods, but they can be easily transformed using the same process (you can define a method as computed property, which returns a function).

## Step 2: Desugar class syntax using the prototype

The class syntax is nothing more than syntactical sugar on top of the function and its prototype. Every class definition has `prototype` property, which holds the class methods (expect `constructor`). What is important, we can change it after the definition, so the body of the class can be empty. Properties can be defined directly on the `MyElement.prototype` using `Object.defineProperty()` method. The prototype delegation may work unexpected with normal values, so we should define only computed properties, which return values related to the context.

```javascript
class MyElement extends HTMLElement {}

// before: this.firstName in constructor()
Object.defineProperty(MyElement.prototype, 'firstName', {
  get: function get() {
    return this._firstName || 'Dominik';
  },
  set: function set(val) {
    this._firstName = val;
  },
  configurable: true,
});

// before: this.lastName in constructor()
Object.defineProperty(MyElement.prototype, 'lastName', {
  get: function get() {
    return this._lastName || 'Lubański';
  },
  set: function set(val) {
    this._lastName = val;
  },
  configurable: true,
});

// before: fullName computed property in the class body
Object.defineProperty(MyElement.prototype, 'fullName', {
  get: function fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  configurable: true,
});

customElements.define('my-element', MyElement);
```

It may seem that we have taken a step back. The code has become more verbose and redundant (A simple structure of the class definition was one of the reasons for the introduction of the class syntax). Also, the current implementation is not consistent with the original one. If we set one of the properties to falsy value, it will still return a default value. We'll take care of that in the fifth step. For now, we have to focus on cleaning out our definition.

## Step 3: Hide redundant code into the custom definition

All properties are defined by the `Object.defineProperty()` method. We can extract passed arguments to a map of property names and descriptors, and put the rest into the custom function, which will replace `customElements.define()` method.

```javascript
const MyElement = {
  firstName: {
    get: function get() {
      return 'Dominik' || this._firstName;
    },
    set: function set(val) {
      this._firstName = val;
    },
  },
  lastName: {
    get: function get() {
      return 'ConFrontJS' || this._lastName;
    },
    set: function set(val) {
      this._lastName = val;
    },
  },
  fullName: {
    get: function fullName() {
      return `${this.firstName} ${this.lastName}`;
    },
  },
};

defineElement('my-element', MyElement);
```

This is how the **property descriptors** concept works. The `MyElement` is now a plain object with a map of property descriptors, which we define on the custom element prototype.

Our `defineElement()` function could be defined like this:

```javascript
function defineElement(tagName, descriptors) {
  class Wrapper extends HTMLElement {}
  
  Object.keys(descriptors).forEach(key => {
    Object.defineProperty(Wrapper.prototype, key, {
      ...descriptors[key],
      configurable: true,
    });
  });

  return customElements.define(tagName, Wrapper);
}
```

## Step 4: Get rid of "this"

The custom function opens the way for further optimization. From now, we have all control over the structure of the input. Instead of passing through property descriptors to `Object.defineProperty()`, a function can create them dynamically. We can finally kill the last standing bastion - `this` keyword.

The first argument of `get` and `set` methods may become a `host` - an element instance. Because of that, we no longer have to access a custom element instance by `this` keyword. Moreover, methods are [pure](https://en.wikipedia.org/wiki/Pure_function) - they depend only on arguments and have no side effects. Removing context also allows using some of the useful features of ES2015 like arrow functions and destructuring function parameters.

```javascript
const MyElement = {
  firstName: {
    get: ({ _firstName }) => _firstName || 'Dominik',
    set: (host, value) => { host._firstName = value; },
  },
  lastName: {
    get: ({ _lastName }) => _lastName || 'Lubański',
    set: (host, value) => { host._lastName = value; },
  },
  fullName: {
    get: ({ firstName, lastName }) => `${firstName} ${lastName}`,
  },
};
```

Our definition has shrunk significantly. We have replaced ordinary functions with arrow functions, and the `host` parameter has been destructured for the `get` calls.

## Step 5: Add middleware to save property value

A computed property by design doesn't hold its value. The definition is a pair of functions (not values), which one of them returns the current state of the property taken from external dependencies, and second updates those external dependencies. In our current solution `firstName` and `lastName` properties depend on `_firstName` and `_lastName` properties from the custom element instance (they are defined when `set` method is invoked for the first time).

Using the fact from the third step, we can introduce a local variable during the property definition in our custom define function. The value can be passed to `get` and `set` methods as a new last argument - `lastValue`. From now, `get` and `set` methods should return the current value of the property.

```javascript
const MyElement = {
  firstName: {
    get: (host, lastValue = 'Dominik') => value,
    set: (host, value, lastValue) => value,
  },
  lastName: {
    get: (host, lastValue = 'Lubański') => value,
    set: (host, value, lastValue) => value,
  },
  fullName: {
    get: ({ firstName, lastName }) => `${firstName} ${lastName}`,
  },
};
```

You can notice how default values are handled now. We've started using another ES2015 feature - default parameters. Those arguments are initialized with default values if no value or `undefined` is passed. It's much better than the solution with `||` operator. Although, the `firstName` and `lastName` sill return `Dominik` or `Lubański` if we set them to `undefined` (In a real-world scenario, it is not a problem, as we can use a built-in factory from the library, which covers that case).

## Step 6: Introduce property factory

After all of the optimizations, we can find redundant code again - `firstName` and `lastName` property descriptors have become almost the same. Only a default value is different. To make it cleaner and simpler we can create a function - property factory, which returns property descriptor parameterized by the arguments.

```javascript
export function property(defaultValue) {
  return {
    get: (host, lastValue = defaulValue) => value,
    set: (host, value) => value,
  };
}
```

We can now replace `firstName` and `lastName` descriptors with `property()` function invocation:

```javascript
import property from './propertyFactory';

const MyElement = {
  firstName: property('Dominik'),
  lastName: property('Lubański'),
  fullName: {
    get: ({ firstName, lastName }) => `${firstName} ${lastName}`,
  },
}
```

With the **property factories** concept, we can define properties with only one line of code! Factories hide implementation details and minimize redundant code.

## Step 7: Introduce property translation

We still have the last concept to follow. Our custom define function takes only descriptors, which are objects with pre-defined structure. What could happen if we allowed passing primitives, functions, or even objects, but without defined methods?

The **property translation** concept provides a set of rules for translating property definition that does not match property descriptor structure. It supports primitives, functions, or even objects (without descriptors keys). 

For example, if we set the value of the `firstName` property to a primitive, the library uses the built-in `property` factory to define it on the prototype of the custom element. In another case, if you set property value as a function, it is translated to a descriptor object with `get` method. 

In the result, custom element definition can be a simple structure of default values and pure functions without external dependencies:

```javascript
const MyElement = {
  firstName: 'Dominik',
  lastName: 'Lubański',
  fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`,
}
```

## Summary

Here is the end of today's coding journey. In the last step we have created the simplest possible definition without `class` and `this` syntax, but with truly composable structure with pure functions. 

The whole process has shown that it is possible to replace imperative and stateful class definition with a simple concept of property descriptors. The other two, property factories and property translation, allow simplifying the definition either further.

## What's next?

Usually, custom elements do much more than our simple example. They perform async calls, observe and react to changes in the internal and external state and many more. To cover those features, component-based libraries introduced sophisticated lifecycle methods and mechanisms for managing external and internal state. What would you say if all of that was no longer needed?

In the next post of the series, we will go deeper into the property descriptor definition and know more about the cache mechanism, change detection and independent `connect` method.

{% link smalluban/how-to-say-goodbye-to-lifecycle-methods-and-focus-on-productive-code-175 %}

You can read more about the hybrids library at the [project documentation](https://hybrids.js.org).

{% github https://github.com/hybridsjs/hybrids %}

---------

🙏 **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 second blog post ever written - any kind of feedback is welcome ❤️.

<small>Cover photo by [Patrick Robert Doyle](https://unsplash.com/photos/sWGDbVPcPdg) on [Unsplash](https://unsplash.com)</small>