DEV Community

loading...
Cover image for Migrating a React codebase to web components

Migrating a React codebase to web components

Maroun Baydoun
Frontend engineer and Mentor.
・5 min read

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 and lit 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 on Chrome and check the code 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 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 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:

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

Read more about the 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:

const CapitalisedText = ({ text }) => {
  return <div>{text.toUpperCase()}</div>;
};
Enter fullscreen mode Exit fullscreen mode

Web components:

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

    this.appendChild(div);
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

<template id="capitalised-text-template">
  <div></div>
</template>
Enter fullscreen mode Exit fullscreen mode

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

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

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 with React to avoid style collisions:

import styles from "./stlyle.css";

const CapitalisedText = ({ text }) => {
  return <div className={styles.text}>{text.toUpperCase()}</div>;
};
Enter fullscreen mode Exit fullscreen mode

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

<template id="capitalised-text-template">
  <style>
    .text {
      font-weight: 600;
    }
  </style>
  <div class="text"></div>
</template>
Enter fullscreen mode Exit fullscreen mode
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()));
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Web component:
Structure of the dom elements of a web component

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.

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?

Discussion (18)

Collapse
sandhilt profile image
Bruno Ochotorena

Vanilla will always be the web standard, jQuery for example was a guide to JavaScript maturity.Just as axios is for fetch, the tendency is always for some library or framework to be swapped for the native.

Collapse
maroun_baydoun profile image
Maroun Baydoun Author

I hope more and more people will see that.

Collapse
clamstew profile image
Clay Stewart

I think they can replace them in a technical sense. I don’t think we’ll see mass migration, refactoring budgets, or dev knowledge growth in that area, without something big happening. Oh the “vanilla framework” is in the hype cycle. And there are future promises hanging out there like react seamlessly solving the pain points around SSR combined with CSR.

Collapse
jfbrennan profile image
Jordan Brennan

I'm hopeful. As the React community sobers up, they will see how the other half has been living for the last 5 years. Sobriety can be hard though, we're here to support you through this difficult but worthwhile change 😁

Collapse
bennypowers profile image
Benny Powers 🇮🇱🇨🇦

Respectfully, I think your outlook might be coloured too much by social media.

Web components adoption, as measured in code running on page loads in browsers, is rising steadily.

arewebcomponentsathingyet.com/

Collapse
aspiiire profile image
Aspiiire

Wow really great article, it makes a lot of sense for you since you are working on a plugin and the support is a little bit more open than for who want to use web components for web development. Thanks to your article now I'm thinking about it, I love vanilla Js and I prefer it, in my opinion at the end it depends, but when I can vanilla is the way to go, with web components mmm would be amazing more support for other browsers

Collapse
maroun_baydoun profile image
Maroun Baydoun Author

That's true, browser support could be a concern for traditional web applications and websites that need to target a wider array of browsers and users. The browser support is not bad overall however. There is definitely a case for using web components in production with polyfills if needed.

Collapse
ivanjeremic profile image
Ivan Jeremic

Take a look at Svelte.

Collapse
jfbrennan profile image
Jordan Brennan

Yes, and Riot and Vue. But why do we insist on avoiding native solutions even when they're good and perfectly useful for the given problem?

Collapse
thirdender profile image
thirdender

Having coded for IE6 "back in the day" and watched the slow-but-inevitable rise of jQuery, I think there are three reasons. First, browser implementation is always just a little different. We're spoiled now that almost all major browsers run on Blink or WebKit, but Gecko can still be temperamental at times. Using a community supported library means it is battle tested against all popular engines and a good library will "just work". Second, the best features are always almost ready. Even as HTML5 was being standardized, it took so long the standard itself was forked. Libraries have the benefit of being able to shim the best features and remove the shims when native support is widely available. Third, knowing and using the DOM API is almost always necessary for native features, and that is in and of itself a pain point still.

Native solutions should always be the goal. But libraries are part of the natural evolution of features that developers actually want to use.

Thread Thread
maroun_baydoun profile image
Maroun Baydoun Author

Knowing and using the native DOM APIs are a necessary "pain". I see people all the time jumping on the React/Vue etc. bandwagon without learning the basics of how browsers work and how to manipulate the DOM. The DOM APIs are not some sort of low-level machine code. They're highly abstracted and usable. Yes, they can be verbose and yes they can be confusing sometimes but learning them is way easier than grasping how React/Vue work.

I wrote about the topic a while ago because I see the issue happening all the time. maroun-baydoun.com/blog/dont-start...

Collapse
ivanjeremic profile image
Ivan Jeremic • Edited

You can't avoid this, frameworks will always exist. React for example is more powerful then web-components.

Thread Thread
jfbrennan profile image
Jordan Brennan • Edited

Yes you can "when [native solutions] are good and perfectly useful for the given problem". His post is literally an example of this, i.e. Web Components were a better solution than React for this problem. Not everything is a React problem needing a React solution.

We need to stop defaulting to "I need to install something" and start thinking "I'll bet there's a good native solution for this".

Thread Thread
maroun_baydoun profile image
Maroun Baydoun Author

You can't avoid this, frameworks will always exist.

That's true, they'll always exist. It doesn't mean, however, that they should be used in every codebase.

Collapse
codingchili profile image
Robin Duda

Nice, I like the move to using the platform features. I'm using custom elements with lit-html for the tagged template literal html function, really cleans up the construction of the DOM and still is very close to platform.

Collapse
jfbrennan profile image
Jordan Brennan

I realised I was over-engineering my solution

You made it to the other side, brother. We welcome you!

Collapse
maroun_baydoun profile image
Maroun Baydoun Author

It only took me 5 years...

Collapse
syncro profile image
syncro

hi, I've developed a library and set if widgets, so you can have bindings, di and extensibility with web components

npmjs.com/package/skinny-widgets