lamplightdev

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:

So Web Components are certainly at a disadvantage here compared to framework components, but they still offer two main advantages:

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.