Migrating a React codebase to web components
Tags: javascript, webdev
Published: 2021-08-02 17:05:08

I recently migrated a browser extension from React to web components. This post describes my observations, learnings and pain points. Anything mentioned applies to native web components only. Third-party libraries such as [stencil](https://stenciljs.com/) and [lit](https://lit.dev/) offer a different set of solutions. 

### What does the extension do?
The extension controls the playback of Youtube™ videos from any tab in the browser. The user can assign shortcuts to play/pause videos even when the browser is in the background. 

You can [install the extension](https://chrome.google.com/webstore/detail/youtube-controller/poalplpedjpddipkghmlhkmgbcnmecop) on Chrome and [check the code](https://github.com/maroun-baydoun/youtube-controller-extension) on Github.

### Why did I migrate?
The extension was originally written using vanilla JavaScript in 2015. As the codebase grew in complexity, it became difficult to maintain the UI. I wanted a tiny framework to help me organise the interface into components. I chose [hyperapp](https://github.com/jorgebucaran/hyperapp) for its small bundle size and seemingly easy API. It wasn’t that straightforward to learn after all and the documentation was lacking at the time. 

A few months later, I migrated to React. I wanted a familiar library that I didn’t need to relearn from scratch. I was finally happy with the quality of the code. The more I thought about it, however, the more I realised I was over-engineering my solution. 

Do I really need to ship two rather large packages, `react` and `react-dom`? The answer was no, even though extensions are loaded locally and package size is not a major concern. The other problem was minification. While minified code is not prohibited, it could [delay the review process](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/What_next_#your_development_environment  ) on some platforms. 

I needed a light-weight solution that doesn’t rely on external libraries. Enter web components.

### Overview of web components
Web components are a set of four standards that have very good [browser support](https://caniuse.com/?search=web%20components):

- Custom HTML elements
- Shadow DOM
- Templates
- EJS modules

Read more about the [specs](https://www.webcomponents.org/specs).

### Comparing web components to React

The following is a list of things I’ve learned during the migration.

#### Custom elements are classes

Since the release of hooks, I have completely stopped writing class components in React. Custom elements can only be declared as classes however. They have specific methods to hook (no pun intended) into the lifecycle of the element. In that sense, they are quite similar to class components in React. One of the biggest differences is the lack of a render method. 

#### Back to imperative programming

Building React components boils down to describing the end result and letting the library take care of the rest. This is done in the render method of class components or as the returned result of functional ones. Custom elements, on the other hand, require direct DOM manipulation to achieve the same result. DOM Elements are queried, created, inserted and modified. 

React:
```jsx
const CapitalisedText = ({ text }) => {
  return <div>{text.toUpperCase()}</div>;
};
```

Web components:
```js
class CapitalisedText extends HTMLElement {
  connectedCallback() {
    const text = this.getAttribute("text");
    const div = document.createElement("div");
    div.appendChild(document.createTextNode(text.toUpperCase()));

    this.appendChild(div);
  }
}
```

#### No binding in templates

Templates are not equivalent to the rendering blocks of React components. It is not possible to pass and render JavaScript data. Nor is it possible to run conditions or loops. All of that has to happen in the custom element lifecycle methods. 


A template defined in the document:

```html
<template id="capitalised-text-template">
  <div></div>
</template>
```

The web component consumes the template but has to do the necessary DOM updates:

```jsx
class CapitalisedText extends HTMLElement {
  connectedCallback() {
    const template = document.querySelector("#capitalised-text-template");
    this.appendChild(template.content.cloneNode(true));

    const text = this.getAttribute("text");
    const div = this.querySelector("div");
    div.appendChild(document.createTextNode(text.toUpperCase()));
  }
}
```


#### Out of the box css scoping

Many solutions exist to scope css in React components. CSS modules, different CSS-in-JS libraries etc. Using the shadow dom in custom elements comes with out-of-the-box support for that. Styles defined in the custom element don’t leak out to the rest of the document, and styles declared elsewhere in the document don’t leak into the custom element. It is a powerful feature when writing reusable components but can be restrictive in other scenarios. It is always possible to write custom elements without shadow DOM however.

Using [css modules](https://github.com/css-modules/css-modules) with React to avoid style collisions:

```jsx
import styles from "./stlyle.css";

const CapitalisedText = ({ text }) => {
  return <div className={styles.text}>{text.toUpperCase()}</div>;
};
```

Using the shadow DOM in the web component to encapsulate styles:

```html
<template id="capitalised-text-template">
  <style>
    .text {
      font-weight: 600;
    }
  </style>
  <div class="text"></div>
</template>
```

```js
class CapitalisedText extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "open" });

    const template = document.querySelector("#capitalised-text-template");
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
  connectedCallback() {
    const text = this.getAttribute("text");
    const div = this.shadowRoot.querySelector("div");
    div.appendChild(document.createTextNode(text.toUpperCase()));
  }
}
```

#### Custom elements remain in the DOM tree

When React renders a component, it creates and appends DOM elements. The component itself is nowhere to be seen in the DOM tree. Custom elements are part of the DOM tree. This is important to note when considering querying and styling. 

React:
![Structure of the dom elements of a React component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eg246qnlvq61rf0308pu.png)

Web component:
![Structure of the dom elements of a web component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t35l5rkybcn1tdgy356a.png)


#### Attributes vs. properties

React components have props that can be of any data type. Custom elements, like any built-in html element, have attributes that can only contain strings. They also have properties that can contain any data type but can only be set using JavaScript. Learn more about [attributes and properties](https://open-wc.org/guides/knowledge/attributes-and-properties/).



#### Listening to attribute changes is opt-in

A react component re-renders when prop values change. Web components expose the `attributeChangedCallback` that can be used to update the UI in response to changes in attributes. This callback doesn’t fire by default however. Every web component has to explicitly list the attributes it wants to listen to using the `observedAttributes` static method.

### Conclusion
Conclusion
Web components are surprisingly good at managing a code base using vanilla JavaScript. Some developers can perceive the current standards as bare-bones however. In fact, the term *components* is somewhat misleading. It draws parallels with existing frameworks and libraries whose components have many more features.

Overall, I'm satisfied with my decision to migrate to web components. I'll definitely use them again for other projects. 

What's your opinion on web components? Do you think they can replace tools such as React or Vue?