On Using Web Component Libraries

We tend to think of components as things that belong to a framework. After all, React has components, Vue has components, Angular has components…it's just how we've always used them.

Because of that, people tend to refer to Lit and FAST Element as frameworks, but they’re not. They’re libraries, and that’s an important distinction.

If you want a React component to work, you have to use it with React. If you want a Vue component to work, you have to use it with Vue. If you want an Angular component to work…well, you get the point.

With web components, the platform is the framework.

Naturally, a follow up question is "why do you need a library then?" The truth is that we don’t. We can create web components without a library. Here's a counter component written in pure JavaScript.

class MyCounter extends HTMLElement {
  static get observedAttributes() {
    return ['count'];
  }

  constructor() {
    super();
    this.state = {
      count: 0
    };
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <button type="button">
        Count:
        <span class="count">${this.state.count}</span>
      </button>
    `;
    this.handleClick = this.handleClick.bind(this);
  }

  connectedCallback() {
    this.shadowRoot.querySelector('button').addEventListener('click', this.handleClick);
  }

  disconnectedCallback() {
    this.shadowRoot.querySelector('button').removeEventListener('click', this.handleClick);
  }

  get count() {
    return this.state.count;
  }

  set count(newCount) {
    this.state.count = newCount;
    this.update();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'count') {
      this.state.count = Number(newValue);
      this.update();
    }
  }

  handleClick() {
    this.count = this.count + 1;
  }

  update() {
    this.shadowRoot.querySelector('.count').textContent = this.state.count;
  }
}

customElements.define('my-counter', MyCounter);

We choose to use libraries to improve the the component authoring experience and abstract messy boilerplate into efficient, reusable modules. Here's a functionally equivalent counter built with Lit.

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

@customElement('my-counter')
class MyCounter extends LitElement {
  @property({ type: Number }) count = 0;

  handleClick() {
    this.count++;
  }

  render() {
    return html`
      <button type="button" @click=${this.handleClick}>
        Count: ${this.count}
      </button>
    `;
  }
}

Sure, we can bake features such as declarative rendering and reactivity into each and every component, but that’s not DRY. It would convolute the code and make our components larger and more difficult to maintain. That’s not what I want and it's probably not what my users want.

Alternatively, we could build those features ourselves and split them off into reusable modules — but that's just reinventing the wheel, isn't it?

When you think of it that way, using a library to build web components makes a lot of sense.


Aside: It’s been said that developer experience is the only benefit to using a library. While it’s true that benefits to the end user are marginalized with one-off components, it's worth noting that APIs such as those offered by Lit and FAST Element lead to less bugs due to reduced complexity and less code in the component itself. Consider the counter examples above. Which one is easier to maintain?