ProjectsLibrary

webcomp.js

Build Web Components using Preact.

What The Hell Is This Thing?

While building a fairly big project at StoreHub we hit a frontend problem. The thing needed to be mostly static, with a bunch of small dynamic bits. We were morally opposed to jQuery, so we started writing tiny React components.

That approach did not scale. It was going to turn into a hack-fest. We needed a new hero. And that hero was…

Web Components

Web Components were finally real. Contained, reusable, exactly what we needed.

The implementations available were kinda meh. Google's Polymer Project is a pain to drop into an existing app. Everything else felt weird. So I made my own.

Requirements:

Familiarity

I didn't want a whole new thing the team had to learn. This library is a utility — you know, “use the platform™.”

There's already a framework that does components well: React. Not Web Components, but the mental model is the best one I've seen. That's the approach I took.

b smol

Nobody wants a utility the size of Angular. Whole thing needed to stay around 10kb gzipped.

Fast and Smart

Fast. Probably a Virtual DOM. Capable of more than rendering HTML strings. Some kind of state.

Under the Hood

After looking at the specs I thought, man, React already does most of this. But I didn't want a React dependency. I wanted a script tag and go. I still wanted JSX. Then it hit me.

Preact. Everything React does, a few extras, 3kb gzipped, time-proven. Preact is the core. The rest of the library lets you write Web Components on top.

The Core

A small set of functions to create and register Web Components, plus utilities for components that aren't custom elements but share the same API. Easy to read, easy to bundle.

Show Me The Code

Here's how you'd write and render a React component back then:

jsx
import { Component } from 'react';
import ReactDOM from 'react-dom';

class CoolHeader extends Component {
  render() {
    return <h1>{this.props.content}</h1>
  }
}

const container = document.getElementById('app');

ReactDOM.render(<CoolHeader content="Pew pew world!" />, container);

And then in your HTML you need that container somewhere:

html
<div id="app"></div>

Same thing as a Web Component with WebComp:

jsx
import { WebComponent, register } from '@webcomp/core';

class CoolHeader extends WebComponent {
  render({ content }) {
    return <h1>{content}</h1>
  }
}

register(CoolHeader, 'cool-header');

And in your HTML:

html
<cool-header content="Pew pew world!"></cool-header>

Pretty cool. If you've written React you'll notice a small convenience right away — props and state are passed into render() so you can destructure them.

Features

  • JSX. Because JSX is awesome
  • React-like syntax and component lifecycle methods
  • Element lifecycle methods
  • Flags. Namespaced “service” props
  • Virtual DOM
  • Shadow DOM
  • Link state for inputs
  • Attribute to props mapping
  • Event-based communication between components
  • State sharing (context)
  • Routing
  • Tiny bundle size
  • Babel preset
  • React Dev Tools integration

The exciting bits:

String Renderer

You don't need a build system to get started. A stateless component in JSX:

jsx
export default ({ content }) => (
  <div>
    <h1>Extended Header</h1>
    <h2>{content}</h2>
  </div>
);

In WebComp you can write it as a string:

jsx
export default ({ content }) => `
    <div>
      <h1>Extended Header</h1>
      <h2>${content}</h2>
    </div>
`;

No JSX, no bundler, still a Virtual DOM.

Shadow DOM

Shadow DOM encapsulates DOM and styles inside the custom element. A DOM inside your DOM. WebComp supports open and closed shadow roots.

Linked state

Forget onChange handlers for inputs. Every component has linkState:

jsx
import { WebComponent } from '@webcomp/core';

class CoolHeader extends WebComponent {
  render({ content }) {
    return (
      <div>
        <span>{this.state.preview}</span>
        <input type="text" onChange={this.linkState('preview')} />
      </div>
    )
  }
}

Events

Sometimes components need to talk. In an SPA you'd have Redux and a provider wrapping the tree. That works for React apps. Web Components are self-contained — any number of instances, none of them know about the others. Different approach.

WebComp has an event system on CustomEvents (“use the platform™” again). A button that changes another component's header color:

jsx
import { WebComponent, Event } from '@webcomp/core';

class SuperButton extends WebComponent {
  @Event('COLOR:CHANGE')
  changeHeaderColor(newColor) {
    return { color: newColor }
  }

  render() {
    return (
      <button onClick={() => this.changeHeaderColor('#f00')} />
    )
  }
}

Then in the other component:

jsx
import { WebComponent } from '@webcomp/core';

class SuperHeader extends WebComponent {
  state = {
    color: 'white',
  }

  componentDidMount() {
    this.on('COLOR:CHANGE', this.changeColor);
  }

  changeColor(color) {
    this.setState({ color });
  }

  render() {
    return (
      <h1 style={{ color: this.state.color }}>My Super Header!</h1>
    )
  }
}

Handlers passed to this.on() were autobound. That's data sharing between Web Components in WebComp.

Conclusion

Already too long, so I'll stop. If you want the rest — client-side routing, shared context, more — the official docs have it.

Next

DailyWall

App