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:
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:
<div id="app"></div>Same thing as a Web Component with WebComp:
import { WebComponent, register } from '@webcomp/core';
class CoolHeader extends WebComponent {
render({ content }) {
return <h1>{content}</h1>
}
}
register(CoolHeader, 'cool-header');And in your 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:
export default ({ content }) => (
<div>
<h1>Extended Header</h1>
<h2>{content}</h2>
</div>
);In WebComp you can write it as a string:
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:
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:
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:
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.
