How to server side render Web Components
Sat Jul 20 2019
Almost all modern frameworks provide a way to server side render (SSR) a web site by running the framework code on a JavaScipt server side framework, such as express, to produce an intial string of HTML that can be sent to the browser. The same component code that runs in the browser is also run on the server.
With Web Components there are several limitations when it comes to SSR:
- it's not possible to run the same code on the server since Web Components rely on browser specific DOM APIs that are not available unless you fire up a headless browser or an alternative DOM implementation. Both of these solutions bring a non-trivial overhead that is hard to justify.
- Shadow DOM cannot (currently) be represented declaratively so you cannot send it over in your initial string of HTML. Instead you'll need to implement some other style encapsulation solution (much like the frameworks do.)
So Web Components are certainly at a disadvantage here compared to framework components, but they still offer two main advantages:
- less code that runs faster as they use built in platform APIs
- future proof as they can be used anywhere, including within current and future frameworks, without modification or risk of obsolesence.
So if these advantages are enough for you to choose Web Components over framework components, how can you SSR Web Components? Well since Web Components without the Shadow DOM are just standard HTML, it's straightforward.
We'll use a simple Twitter share button as an example. To see the final result have a look at the demo (or browse the code) and try with and without JavaScript enabled.
On the client
Our initial, browser based, web component may look something like this:
twitter-share.js
class TwitterShare extends HTMLElement {
connectedCallback() {
// set up the props based on the inital attribute values
this.props = [...this.attributes].reduce((all, attr) => {
return {
...all,
[attr.nodeName]: attr.nodeValue,
};
}, {});
this.render();
}
render() {
// set the innerHTML manually - ShadowDOM can't be used
this.innerHTML = this.template();
// add an event listener to the link inside the component
const a = this.querySelector('a');
a.addEventListener('click', this.open);
}
template() {
// create the HTML needed for the component
const { text, url, hashtags, via, related } = this.props;
const query = [
text && `text=${encodeURIComponent(text)}`,
url && `url=${encodeURIComponent(url)}`,
hashtags && `hashtags=${hashtags}`,
via && `via=${encodeURIComponent(via)}`,
related && `related=${encodeURIComponent(related)}`,
]
.filter(Boolean)
.join('&');
const href = `https://twitter.com/intent/tweet?${query}`;
return `
<a target="_blank" noreferrer href="${href}">
Tweet this
</a>
`;
}
open(event) {
// open the twitter share url in a popup window when the link is clicked
event.preventDefault();
const a = event.target;
const w = 600;
const h = 400;
const x = (screen.width - w) / 2;
const y = (screen.height - h) / 2;
const features = `width=${w},height=${h},left=${x},top=${y}`;
window.open(a.getAttribute('href'), '_blank', features);
}
}
customElements.define('twitter-share', TwitterShare);
However, to render this on the server you can't use HTMLElement
, customElements
or addEventListener
for example as those APIs don't exist on the server, so you'll need to extract the template method into a standalone function that can be used:
twitter-share-template.mjs
// .mjs is used here so javascript modules can be used on the server
export default (props) => {
// the same logic as above, in a self contained function
const { text, url, hashtags, via, related } = props;
const query = [
text && `text=${encodeURIComponent(text)}`,
url && `url=${encodeURIComponent(url)}`,
hashtags && `hashtags=${hashtags}`,
via && `via=${encodeURIComponent(via)}`,
related && `related=${encodeURIComponent(related)}`,
]
.filter(Boolean)
.join('&');
const href = `https://twitter.com/intent/tweet?${query}`;
return `
<a target="_blank" noreferrer href="${href}">
Tweet this
</a>
`;
};
and update our browser based component definition to import and use that function:
twitter-share.js
import template from './twitter-share-template.mjs';
class TwitterShare extends HTMLElement {
//...as before
render() {
this.innerHTML = template(this.props);
// ...as before
}
// template() -> removed
// ...as before
}
customElements.define('twitter-share', TwitterShare);
On the server
On the server, while constructing the page response you need to use the HTML tag for our component with the necessary attributes that will be parsed by the browser, and for the SSR content of the component use the same template
function as above:
server.mjs
import template from './public/js/twitter-share-template.mjs';
// ... the server side route
const props = {
text: 'A Twitter share button with progressive enhancement',
url: 'https://grave-mirror.glitch.me',
via: 'lamplightdev',
};
// be sure to sanatize the props if including directly in HTML
response.write(`
<twitter-share text="${props.text}" url="${props.url}" via="${props.via}">
${template(props)}
</twitter-share>
`);
// ...
To see the component in action have a look at the demo and browse the code. Be sure to try the page with and without JavaScript enabled.
In summary, you can SSR web components without any server side DOM implementation, but only without the Shadow DOM.