The Case for React-like Web Components

I dig React. My favorite feature of React is the emphasis on building small blocks of UI in JSX, and then combining those pieces together to create an app.

Two years ago I wrote about how the choice for Vanilla JavaScript is more compelling than ever before because of new technologies such as Fetch, Promises, template literals, and ECMAScript Modules. That last one is key, because at the time of my post no browser actually supported module loading.

Today, the situation is thankfully much different. Edge 16+, Chrome 61+, and Safari 10.1+ all support ES Modules natively! Firefox is the odd duck out (relevant bug here), but feature detection for ES Modules is really easy (use <script nomodule> for unsupported browsers and <script type=”module”> for supported ones.)

Combined with existing advances such as ES Classes, template literals, and Web Components (specifically Custom Elements and Shadow DOM), ES Modules were the final piece of the puzzle for React-like self-contained UI components.

Disclaimer

Creating a React-like Web Component

class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

Thanks to JSX, you’re able to easily compose HTML and JavaScript together.

Let’s look at the Vanilla JS version using ES Modules and Web Components:

export class GreetingView extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<h1>Hello, ${this.getName()}</h1>`;
}
// Other code, including getName()...
}
window.customElements.define('greeting-view', GreetingView);

As you can see, it’s not as elegant and there are some problems (addressed below) which detract from making a direct 1:1, line-for-line comparison. But this is still really powerful for a “bare-metal” JavaScript implementation.

Note: Custom Elements must contain a dash between the first and last characters. This is to differentiate them from the built-in HTML elements such as <input>, etc. It’s good practice to namespace your Web Components (the examples in this post use “fancy-*,” for example.)

Composing with Web Components

Let’s say we want to make our own, better <button> element:

export class FancyButton extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<button>
<slot></slot>
</button>
`;
}
}
window.customElements.define('fancy-button', FancyButton);

In Web Components, you can declare <slot> elements which will contain the HTML between your custom element’s opening and closing tags. Let’s see that in action:

import {FancyButton} from './FancyButton.js';export class FancyView extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<article>This is my view!</article>
<fancy-button>This is my fancy button</fancy-button>
`;
}
}
window.customElements.define('fancy-view', FancyView);

In this example, the text “This is my fancy button” will be inserted into our Web Component’s <button> element.

You can also take the programmatic approach:

import {FancyButton} from './FancyButton.js';export class FancyView extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<article>This is my view!</article>
`;
const button = new FancyButton();
shadowRoot.appendChild(button);
}
}
window.customElements.define('fancy-view', FancyView);

Composing HTML pages using DOM elements has always been a part of the Web, and Web Components build on that rock-solid foundation.

Note: <slot> does not seem to be supported in Edge yet. 😓

Modularized CSS

shadowRoot.innerHTML = `
<style>
:root {
box-shadow: 1px 1px 1px gray;
}
h1 {
color: red;
}
</style>
<article>
<h1>Hello, ${this.getName()}</h1>
<p>
${this.getContent()}
</p>
</article>
`;

A caveat is that because styles don’t penetrate, that means in the example above even if the parent page has this:

button {
background: purple;
}

Our <fancy-button> element will show the default, unstyled <button> element instead. If you want to get that <button> styled from the parent page, you’d have to do something like this:

fancy-button {
background: purple;
}

And then in our Web Component:

shadowRoot.innerHTML = `
<style>
button {
background: inherit;
}
</style>
<button>
<slot></slot>
</button>
`;

However, the inherit property can really only go so far when using it this way.

Unfortunately when using the Shadow DOM, you’re also not able to share CSS Variables across components. That is a bummer. But there is something of a workaround…

Problem A: Styles from the parent page do not cascade into the Shadow DOM

But, if there are some things you really need to cascade down, the following workaround can get you there:

export class GreetingView extends HTMLElement {
connectedCallback() {
this.innerHTML = `<h1>Hello, ${this.getName()}</h1>`;
}
}
window.customElements.define('greeting-view', GreetingView);

this.innerHTML bypasses the Shadow Root and exposes all of your component’s HTML to the parent DOM.

I’ve found this pattern is most useful when you’re building your entire app with Web Components and you don’t expect to to share them outside that project. Otherwise, if you intend to open source your Web Components or use them across projects the Shadow DOM method is the way to go.

Problem B: There’s no data-binding

Problem C: You can’t extend native elments like <button> or <input>

Chromium: “Launch customized built-in elements”

Web Components: “Support extending elements”

As it stands, my efforts above to “build a better <button>” have involved wrapping one inside of a Web Component instead of extending from HTMLButtonElement, which would be far nicer.

“Should I throw out React/Vue/etc. and start over with Vanilla JS?”

However, if your main attraction to React (and Vue, et al) is the ability to compose standalone components with HTML inside, then take a serious look at Web Components. They’re less magic, but are now worth considering for new projects.

If you like this article, please recommend it by clicking the “clapping” icon and by upvoting it on Hacker News and Reddit. 😁