While building a relatively big project back at my old job, 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 are wonderful and magical and they’re finally real, and there’s 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.
Which probably turned out just as weird, but at least I know how it works 😃
So, I set out on a path to build a new solution for writing Web Components. What were the challenges and requirements?
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.
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.
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.
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.
Okay, let me demonstrate. So, here’s how you would normally write and render a React component:
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 have to have that container element somewhere:
<div id="app"></div>
Now, here’s how you write and render the same thing as a Web Component using WebComp:
import { WebComponent, register } from '@webcomp/core';
class CoolHeader extends WebComponent {
render({ content }) {
return <h1>{content}</h1>
}
}
register(CoolHeader, 'cool-header');
And then in your HTML:
<cool-header content="Pew pew world!"></cool-header>
Pretty cool, right?
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.
WebComp actually has 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)
Let’s briefly go through the exciting bits.
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>
);
In WebComp, you can write it as a string like this:
export default ({ content }) => `
<div>
<h1>Extended Header</h1>
<h2>${content}</h2>
</div>
`;
No JSX used, no bundling/build system necessary, and you still get all the cool things like Virtual 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.
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>
)
}
}
Some times 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')} />
)
}
}
Then in your other component:
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>
)
}
}
Handlers passed to this.on() are autobound. Magic!
And that’s how you handle data sharing between Web Components in WebComp.
I feel like it’s already too long, so I’m gonna stop here. If you’re interested to know more about WebComp, you can check out the official docs. Features not mentioned here include routing, shared context and more good stuff.
Be sure to check out my other work, you might find my other projects interesting as well.