Building Custom Elements With a Library

I often get asked why I use Lit to build web components. Hands down, it's become my preference after years of working with various libraries and tools. Here's why I use it.

Libraries offer a better DX #

This seems like a selfish answer for a developer to lean on, but there are advantages to end users as well. A better developer experience (DX) means I can write components and fix bugs faster. In my experience, a lot faster. Lit, for example, maps attributes to properties automatically, meaning I can omit a ton of extra logic from my code.

Abstractions like this make me more efficient at writing components because I can concentrate on the important stuff. And with less code to worry about, I find components are less prone to bugs. If I had to write those mappings myself, for example, it would get very cumbersome. More code to write, more bytes to load, and more complexity to maintain.

Keeping it DRY #

If I were writing vanilla web components, I'd eventually recognize patterns amidst the boilerplate. As any good developer would, I'd split them out and move them into a reusable utility of some sort. That's the DRY thing to do, right?

I've been down that road before. I built my own library to author web components. Declarative templates, reactive data binding, lifecycle hooks — all the shiny things that make building components fun.

I ended up spending more time working on the library than the components. If only there were something out there that did exactly what I was looking for…

Turns out, that was Lit.

Oddly enough, Lit was about the same size as my own library. What I like about Lit is that it's designed to work with the platform, not replace it. Its maintainers are very committed to stability and backwards compatibility, and they have an amazing track record to back this.

For building a one-off custom element, vanilla APIs will work just fine and save you some bytes. But when you start building a collection of them, the size and maintainability arguments fall apart. In my opinion, it's wiser to keep those abstractions well-tested, well-documented, and well-separated from the rest of your code.

Vanilla APIs are intentionally low-level #

The APIs that comprise web components aren't like those like you're used to with React, Vue, et al. Remember, the platform evolves slowly because it has to commit to features for many, many years. This is why webpages from decades ago still work in modern browsers.

Web component APIs took a long time to establish. They were designed to be low-level so developers can build on top of them. As a result, vanilla web components tend to have a lot of boilerplate.

You know how you can build React components without JSX?

// without JSX
import { createElement } from 'react';

function Greeting({ name }) {
  return createElement(
    'h1',
    { className: 'greeting' },
    'Hello ',
    createElement('i', null, name),
    '. Welcome!'
  );
}

Sure, it's possible, but isn't this much easier to work with?

// with JSX
function Greeting({ name }) {
  return (
    <h1 className="greeting">
      Hello <i>{name}</i>. Welcome!
    </h1>
  );
}

Extrapolate this concept to more complex components. Which would you rather build and maintain? In some ways, Lit is to web components what JSX is to React. You can build things without it, but I wouldn't recommend it.

At the end of the day, the way a web component is built is an implementation detail. I can swap out Lit for any other library and, as long as I don't change the public API of my components, most users would never notice.

They're just HTML elements, after all.