What The Hell Is This Thing?
While building a relatively big project back at StoreHub, we faced a little frontend challenge. The project needed to be an app that was primarily statically rendered, but it also needed to have a bunch of small dynamic elements to do certain things. Since we were morally opposed to using jQuery for something like this, we first decided to write a bunch of tiny React components to do these things.
But unfortunately it quickly became apparent that with our particular use case that approach was not exactly scalable and would quickly turn into a hack-fest and maintenance pain. So we needed a new hero. And that hero was...
Web Components
Web Components are wonderful and magical and they were finally real, and there wass no reason not to use them. They’re contained and reusable and that’s just what we needed.
The problem was that all Web Components implementations available were kinda meh. Like, there’s Google’s Polymer Project , but it’s a bit of a pain to integrate into an existing project. And there’s a bunch of other things but they all just felt weird. So I decided to make my own thing and set out on a path to build a new solution for writing Web Components.
What were the challenges and requirements?
Familiarity
I didn’t want to build a whole new massive thing that everyone on the team would have to learn. After all this library is just the utility, because you know, ”use the platform™”.
And there’s a framework out there that also does components that you probably know of - React. Granted, it’s not Web Components, but nonetheless, React’s approach to components is the best one I’ve seen. So that’s the approach I took with this library.
b smol
No one wants a utility library to grow to the size of Angular, so the whole thing needed to be under or around 10kb GZipped.
Fast and Smart
It also definitely needed to be fast. Probably have some sort of Virtual DOM. It also needed to have capability beyond rendering strings of HTML and have some sort of state management solution.
Under the Hood
After looking at all the specs I thought ”man, React does most of this stuff really well!”. But I didn’t want to have a dependency on React. In fact, I wanted users to be able to simply drop a script tag on the page and start developing Web Components. But I still wanted to be able to use JSX and all the niceness of React. And then it hit me.
Preact! It does everything React does with some extra conveniences and it’s 3kb GZipped, so it can be easily bundled and it’s time proven. So I decided to put Preact under the hood. Preact acts as a core and gives you that sweet React experience, while the rest of the library allows you to write awesome Web Components.
The Core
The core of the library is a set of functions that allow you to create Web Components and register them. It also provides a set of utilities that allow you to create components that are not Web Components, but still have the same API.
It’s a very small library, so it’s easy to understand and use. It’s also very lightweight, so it’s easy to bundle and use in your project.
Show Me The Code
Okay, let me demonstrate. So, here’s how you would normally 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);
<div id="app"></div>
import { WebComponent, register } from '@webcomp/core';
class CoolHeader extends WebComponent {
render({ content }) {
return <h1>{content}</h1>
}
}
register(CoolHeader, 'cool-header');
<cool-header content="Pew pew world!"></cool-header>
If you’ve worked with React before you may notice a convenience enhancement right away - props and state of the component are passed as arguments to render() for convenient destructuring.
Features
WebComp actually had quite a few features.
JSX React-like syntax Virtual DOM Shadow DOM Component and element lifecycle hooks Attribute to props mapping Event based communication State sharing (context)
- JSX. Because JSX is awesome
- React-like syntax and component lifecycle methods
- Element lifecycle methods
- Flags. Basiclly they’re namespaced ”service” props
- Virtual DOM
- Shadow DOM
- Link state for inputs
- Attribute to props mapping
- Event-based comminicatoon between components
- State sharing (context)
- Routing
- Tiny bundle size
- Babel preset
- React Dev Tools integration
String Renderer
We’ve already seen JSX, but I also mentioned that you don’t really need a build system to get started with WebComp.
Consider a stateless component:
export default ({ content }) => (
<div>
<h1>Extended Header</h1>
<h2>{content}</h2>
</div>
);
export default ({ content }) => `
<div>
<h1>Extended Header</h1>
<h2>${content}</h2>
</div>
`;
Shadow DOM
One of the coolest features of Web Components is the Shadow DOM. Shadow DOM provides incapsulation for DOM and styles inside the custom element from the main document. Essentially it’s a DOM inside your DOM.
WebComp fully supports open and closed Shadow DOM.
Linked state
Forget writing onChange handlers for inputs. Every WebComp component has a linkState method, that works like this:
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 you need your components to talk to each other. Now, in a single page app, you’d normally have something like Redux handling the data flow and you’d have your entire app wrapped in some sort of provier component. This approach works with React SPAs, but Web Components are self contained, and there may be any number of instances of any number of components on the page, and one component doesn’t know about the others because of the incapsulation. So a different approach was needed for data sharing.
WebComp has a built in event system based on CustomEvents specification (remember, ”use the platform™”) and it’s extremely simple to use. Let’s say you want a button component to change the color of a header in another component. Here’s how you would write that with WebComp:
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')} />
)
}
}
import { WebComponent } from '@webcomp/core';
class SuperHeader extends WebComponent {
state = {
color: 'white',
}
componentDidMount() {
// componentDidMount is a good place to do inits
this.on('COLOR:CHANGE', this.changeColor); // No need to bind
}
changeColor(color) {
this.setState({ color });
}
render() {
return (
<h1 style={{ color: this.state.color }}>My Super Header!</h1>
)
}
}
Conclusion
I feel like it’s already too long, so I’m gonna stop here. If you’re interested to know more about WebComp and other features like client-side routing, shared context and more, you can check out the official docs.
Be sure to check out my other work, you might find my other projects interesting as well.