Lit - Web Components on Fire

Lit - Web Components on Fire

Published:

Web Components are a ready-to-use browser standard to create universally reusable, framework-agnostic components. However, the API is very basic. Lit is here to fix this.

Cover image

What are Web Components?

Most frontend developers are familiar with at least one component-based web framework like Angular, React or Vue now.

Web Components are less popular, but they are a web standard. This means, we don’t have to ship a large library to the client, just to enable JavaScript components.

They provide some nice features out of the box:

What is Lit?

The issue with Web Components is that their API is low-level, and we don’t have things like state management and templating at hand.

It also doesn’t make sense to standardize those because there are many approaches in today’s component libraries and there is no single “right” way to do it.

This is where Lit comes into play: It mainly provides us an opinionated approach to reactive state management and a very performant template mechanism.

Templating

The templating in Lit is a bit unusual. It uses tagged template literals.

Tagged template literals are part of ES-2015. They are simply functions that can be called with a template string.

const template = html`
  <p>Static: 42</p>
  <p>Dynamic: ${42}</p>
  <p
    @mouseenter=${(e: Event) =>
      ((e.target as HTMLParagraphElement).style.background =
        randomColor())}
  >
    Interactive
  </p>
`;

render(template, document.body);

The templating is also available as a standalone npm package, called lit-html . You can use it to efficiently render HTML to any page.

You can use a plugin for your IDE to get syntax highlighting for the HTML strings.

Reactive State Management

State management and change detection in Lit is provided via reactive properties.

Lit distinguishes between @property() and @state() . Properties are public and can be synchronized with an attribute value. State properties are private and hold the custom element’s internal state.

@customElement("my-properties-and-state")
export class PropertiesAndState extends LitElement {
	@property() prop = "fallback";
	@state() private state = 42;

	render() {
    return html`
      <p>Property: ${this.prop}</p>
      <p>State: ${this.state}</p>
			<button @click=${() => this.state--}></button>
    `;
  }
}

By default, the properties are linked to attributes and whenever an attribute changes from outside, the property value will be parsed from this attribute.

This allows you to easily describe the custom element’s API.

At the same time, the decorators will create getters and setters for the properties. The setter will trigger the change detection whenever a value is assigned to the property.

Directives

Directives in Lit help you with the dynamic parts of templates.

You can use them at specific places within your template.

There are some helpful built-in directives , but you can also write your own .

What makes Lit so fast?

Templating with Tagged Template Literals

As mentioned above, Lit uses tagged template literals for templating. A tagged template literal is a function which gets passed in the static and the dynamic parts of the template string separately.

function html(staticStrings: string[], ...values: any[]) {
  // Implementation
}

This makes parsing and (re-)rendering of the templates very efficient because the static parts don’t have to be checked for changes and as such, they only have to be processed once.

Reactive Properties

The reactive properties schedule an efficient change detection mechanism. So if multiple properties are synchronously changed, they are all bulk-checked. Only if one of those properties holds a new reference, the component will be re-rendered.

@customElement("my-reactive-props")
export class ReactiveProps extends LitElement {
  @state() private a = 0;
  @state() private b = 0;

  render() {
    console.log("ReactiveProps rendered");
    return html`
      <p>a: ${this.a}</p>
      <p>b: ${this.b}</p>
      <button
        @click=${() => {
          this.a++;
          this.b = this.a * this.a;
        }}
      >
        Change
      </button>
    `;
  }
}

Fine-grained reactivity with Async Directives

You can use async directives for fine-grained reactivity. The async directives can emit a new template result at a random time without having the surrounding component re-render.

@customElement("my-async-directives")
export class AsyncDirectives extends LitElement {
  #async = new Promise((resolve) =>
    setTimeout(() => resolve("resolved"), 2000),
  );

  render() {
    console.log("async-directives rendered");
    return html`
      ${until(this.#async, html`<span>Loading...</span>`)}
    `;
  }
}

In this example, we use the built-in async directive until() . When the promise resolves, the directive will emit the new value. But the rendering of the AsyncDirectives component is not called again.