Frameworks and integration

Although web components can be used to build a standalone application all on their own, they're missing several of the requirements that would usually be used to do so. There's no built-in library for managing state, templating HTML (the <template> tag is useful but laughably primitive), or handling client-side routing. Those problems weren't what the technology was originally created to solve.

That doesn't mean web components are useless. They still serve as a good way to bundle chunks of functionality into a document, and they're usually smaller and faster than the library equivalent. But it does mean that if you're building something reasonably complex, you may find yourself mixing the two: web components for self-contained UI widgets, and framework code to contain and coordinate those components.

Since most JavaScript frameworks have some sort of component model of their own, a good question is whether it's capable of using web components at all. Of course, since custom elements (especially well-written ones) look like regular DOM elements, most of them should be able to use them in the basic sense of "creating them and setting some attributes." However, Rob Dodson's Custom Elements Everywhere grades real custom element compatibility on two sets of tests:

At the time of this writing, most frameworks handle these challenges just fine, with one unfortunate exception: React. It's perhaps not surprising that React — which prioritizes purity of abstraction over the messy world of the DOM — would require some extra work to handle custom elements, just as it often handles elements like <video> or <canvas> badly. But luckily, there are strategies to work around this, if you want to include a custom element in a React application (and given that the entire point of web components is to be a cross-framework solution for UI widgets, why wouldn't you?).

React integration

If your component only uses attributes, you're in luck: React handles this case by default. You can just set them from JSX as normal:

render() {
  return <div>
    <custom-element value={this.state.value}></custom-element>
  </div>
}

However, if you need to set a non-primitive value, or a property that isn't mirrored to an attribute, you'll need to use a callback ref to get access to the actual DOM element.

render() {
  return <div>
    <custom-element
      ref={element => element && element.data = this.state.customElementData}
    ></custom-element>
  </div>
}

In this code, when our custom element is added to the DOM, React will call our function and pass in the element to be updated. We need the element && guard because the function may also be called with null when the element is unmounted.

This is a frustrating pattern, but it becomes more so if our element dispatches DOM events to communicate about progress. Because React uses an entirely synthetic event system, we will need to use a ref to manually attach and remove event references. In this case, we'll need to create a more complex callback function, likely one that is attached to a class component:

class ReactHost extends React.Component {
  constructor() {
    super();
    this.customElement = null;
    this.onCustomEvent = this.onCustomEvent.bind(this);
  }

  onCustomEvent() {
    // perform whatever we need to from the DOM event
    this.setState({ customEventFired: true });
  }

  updateCustomElement(ref) {
    if (ref) {
      // store a reference for later
      this.customElement = ref;
      // set a property
      ref.data = this.state.customElementData;
      // add a DOM event listener
      ref.addEventListener("custom", this.onCustomEvent);
    } else {
      // if ref is null, the element is going away
      // clean up data and events
      this.customElement.data = null;
      this.customElement.removeEventListener("custom", this.onCustomEvent);
      // nullify our stored reference
      this.customElement = null;
    }
  }

  render() {
    return <div>
      <custom-element ref={r => this.updateCustomElement(r)}></custom-element>
    </div>
  }
}

This is not a great pattern, but it's usable. And I have hope — perhaps futile — that as web components become more common, React will have to become more flexible in the tools it offers for using the DOM, rather than keeping it at arm's length in this way.

Writing web components via frameworks

While we typically think of integrating custom elements into a framework, the opposite can also be true: you can use a framework to write the insides of a web component. While I personally would probably sooner add a library like Lit-HTML, if you're already familiar with one of those frameworks, it's easy enough. Essentially, we can treat the shadow DOM as the mount point for our framework app, just as we would normally do with a <main> tag. For example, here's a simple Preact-based element that issues a greeting, updates from the element attributes, and passes click listeners through to the inner functional component:

<script type="module">
  import { html, render, Component }
    from 'https://unpkg.com/htm/preact/standalone.module.js'

  function InnerComponent(props) {
    return html`<button onClick=${props.clicked}>${props.greeting}</button>`;
  }

  class PreactElement extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
      this.shadowRoot.innerHTML = `
<style>
:host {
  display: block;
  margin: 20px;
  border: 2px dotted red;
}

button {
  display: block;
  width: 100%;
  cursor: pointer;
  padding: 20px;
  background: white;
  border: none;
}
</style>
      `
      this.render();
    }

    static get observedAttributes() {
      return ["greeting"];
    }

    attributeChangedCallback(attr, was, value) {
      switch (attr) {
        case "greeting":
          this.greeting = value;
          this.render();
        break;
      }
    }

    render() {
      var { greeting } = this;
      render(html`<${InnerComponent}
        greeting=${greeting}
        clicked=${() => this.setAttribute("greeting", "Goodbye")}
      />`, this.shadowRoot);
    }
  }

  customElements.define("preact-element", PreactElement);
</script>
<preact-element greeting="Yo"></preact-element>

In some cases, frameworks will even offer tools for generating custom elements directly from your existing modules. Rather than use my own janky translation above, for example, Preact has preact-custom-element, and Vue and Svelte offer vue-web-component-wrapper and a built-in compiler options, respectively.