Web Components for React developers

Petrus Pierre
Petrus Pierre

Fullstack Developer | Working @ TAS

Have you ever wondered why you use React, or better, why you need React?

Do you use it because you need to build web interfaces that are dynamic? Because it is very utilized in the market? Or maybe because it is more productive, you feel that you don't have much boilerplate to worry about? Was it the only way you have discovered so far?

Well, regardless of the reason, this is not a rant on React, in fact, the blog you are reading is built with React, and I do use it every day and see its value. The purpose of this post, though, is to show you how the web advanced and present you with other options that might achieve the goals you are looking for with React or any other web framework.

Recap: How does React work?

If you are already familiar with React and know how it works, feel free to skip this section.

By using React, you have the ability to build interfaces with just Javascript, and from the Javascript code it will manage to create and insert the elements you have described in the DOM.

That will give you some perks when writing code for web interfaces, like creating components that can be reused across your application, and adding interactivity to these components by using state, which are values that can be manipulated to update your component.

So I can create a Counter that will store a count state, and whenever that state is updated, we'll reflect the change in the component view.

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

This is a super simple example, but this is the Core of React, so it will work the same way independently of your component size. So how does it work, exactly?

React needs to store a "Virtual DOM", which is basically a copy of the actual DOM, and whenever a state changes, it triggers a new render, which is basically React building the new DOM elements with the updated values, but still in the "virtual" one, then it runs a reconciliation algorithm to spot check what are the differences in the new "virtual DOM" and the actual DOM, and if there are any differences, it just commit them.

And that's it for the core of React, that's how it handles state changes, it needs an intermediate version of the DOM so it can stop and understand what are you updating and then apply it to the screen, and it might sound complicated to you, but React makes all that really fast, so in most scenarios you can't notice that.

Introducing Web Components

Great, we know we can build dynamic, reusable, and interactive pages using React, now, what about Web Components?

Here's a definition from webcomponents.org

Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.

Great! So this is a way, with native web APIs, to build custom and reusable components for our web applications, this might serve the purpose of the application you want to build. But before taking more conclusions, let's look at some important concepts of Web Components:

Custom Elements

The Custom Element specification defines a set of JavaScript APIs that allow you to create your HTML tags and define custom behaviors.

Based on the State of HTML 2023 the top 1 element the community considers it is missing in HTML, is a Data Table element, so you got the brilliant idea of developing your own one, all you need to do (despite everything else) is the following:

class DataTable extends HTMLElement {}
customElements.define("data-table", DataTable);

And now you can use it, just as any other HTML element!

<data-table></data-table>

In React, usually, we name our components starting with a Capital letter, since that is recommended in the JSX syntax. For a Custom Element, however, you will want to keep everything lowercase and you need to use a dash in the element name or a custom prefix for your application, just to avoid collision!

See the WHATWG specification for Custom Elements.

HTML Template

Now, for me, this is one of the most interesting things about Web Components. With HTML templates, you have two tags: <template> and <slot> that enable you to write "markup templates" that will not render on the page, but can be reused in creating a custom element structure.

So, let's say we want to create our own Button element, in React you probably have already done the following:

export function Button(props) {
  return (
    <button>
      {props.children}
    </button>
  );
}

With custom elements and HTML templates, you can create a <template> and manipulate it in your element. Here's what it would look like:

<template id="my-button-template">
  <button>
    <slot></slot>
  </button>
</template>

<my-button>Click me!</my-button>

<script>
customElements.define('my-button',
  class extends HTMLElement {
    constructor() {
      super();
      const template = document
        .getElementById('my-button-template')
        .content;
      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(template.cloneNode(true));
  }
});
</script>

Don't worry if that feels too much, I will show you how to reduce the boilerplate!

So we have the same behavior we had in React, great! But we can also have more. Slots allow you to define default fallbacks and named slots, so you could have something similar to:

<template id="my-button-template">
  <button>
    <slot>Placeholder</slot>
    <slot name="prefix"></slot>
  </button>
</template>

<my-button>
    Click me!
    <span slot="prefix">I'm a prefix!</span>
</my-button>

See the WHATWG HTML Template specification

Shadow DOM

The Shadow DOM also refers to a set of JavaScript APIs that you can use with your elements, that one is a bit more controversial in the community.

With the Shadow DOM, you can attach an encapsulated "shadow" DOM tree to an element, and by doing that, you can add scripts and styling that will not collide with other parts of the "actual" DOM.

In the end, that means that you can write CSS styles for your element, and they will not affect the others on the page, regardless if you have used a selector that would match other elements outside the "shadow".

The Shadow DOM specification is now getting moved to the DOM specification.

What about the counter component?

Let's rewrite that Counter component we created for React but using Web Components now.

So far, I just presented Web Components with its bare vanilla implementation, however, we have lightweight libraries today to reduce the boilerplate (that probably bothered you in this post). Let's use Lit for this implementation.

import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';

@customElement('my-counter')
export class Counter extends LitElement {
  @state()
  count = 0;

  render() {
    return html`
      <div>
        <p>${this.count}</p>
        <button @click=${() => this.count++}>Increment</button>
      </div>
    `;
  }
}

Now you can just use your my-counter element anywhere you want, with any framework you want. You also just created a state that doesn't rely on a "virtual DOM" and "reconciliation", so you are developing close to a vanilla "native" environment, with fewer abstractions that can make your code slower.

Why would I use it?

Well, you don't need to, as in many other technologies and tools you have available to work with every day. However, it is really important that you build a strong toolset and repertory of what you can do.

This is mainly important because if we don't build a good repertory, we'll end up thinking our tools are silver bullets and can solve every problem, but when you need to make better decisions for your solutions, you need to leverage what is the best fit for each situation.

Using Web Components is great for many things, but personally, I see it shining in scenarios like:

  • Building design systems
  • Working more closely with web-native APIs

In design systems, it can be great, since you are free to implement the components in any other framework you want for the applications that will use it.

Most times when we are working in React, we tend to use library abstractions for many things instead of looking for native solutions or how they actually work, and that is not wrong, however, since with Web Components you are working close to these APIs, it makes sense for you to understand them better and take more advantage of what they can offer.


This was a brief introduction to what Web Components are and what you can do with them. If you want more, stay tuned, I should make more of these at some time! Also, I really recommend Rob Eisenberg's content on his Medium blog.

That's all! I hope that this post brought you something new to study and add to your repertory, helping you to make better choices in your future solutions!