Server Rendering

Hybrid Usage

One use case for server rendered components is to render the initial state on the server and then let the client take over responsibility when the client side JavaScript has loaded.

In this case two component definitions are needed, one for the client and one for the server. Since the API for Wafer components on the client and server is identical, one approach to this is to use a simple mixin to share the code:

const Mixin = (Base, context = {}) => {
return class extends Base {
static get template() {
return 'Hi <span id="firstname"></span>!';
}

static get props() {
return {
firstname: {
type: String,
targets: [
{
selector: "$#firstname",
text: true,
},
],
},
};
}
};
};

The context parameter is useful for passing in data/functions that are context specific - see below for an example.

The server component can be defined for use in node as:

import WaferServer from "@lamplightdev/wafer/lib/server/wafer.js";

class MyExampleServer extends Mixin(WaferServer) {}

const htmlString = "...";

const tree = await parse(htmlString, {
"my-example": {
def: MyExampleServer,
},
});

while the client component can be defined for use in the browser as:

import Wafer from "@lamplightdev/wafer";

class MyExampleClient extends Mixin(Wafer) {}

customElements.define("my-example", MyExampleClient);

When the client code is then loaded in the browser all matching server rendered components (identified by the wafer-ssr attribute that is automatically rendered, unless using server only mode), are automatically 'rehydrated' using the attributes set on the server.

This is why attributes are set for all properties on the server, whether they are reflected or not - they are the source of data for 'rehydration'. Once the component has been 'hydrated' on the client, any unreflected attributes are then removed.

Note that 'hydration' only involves setting properties from attributes and attaching any event listeners. No DOM updates are carried out on the initial render since the DOM has already been created on the server.

How and when the client definition is loaded is up to the particular use case. They could be imported statically from a script, dynamically imported using an IntersectionObserver, or bundled into a single script using any bundler for example.

Example

Open in new window

Context

While the component definition can be shared between the server and client code, sometimes it's necessary to implement behaviour differently depending on whether the component is being rendered on the server or client. In these cases a context parameter can be defined as part of the mixin to enable context specific code to be passed in.

One such example is data fetching - on the client the native fetch is available while it is not on the server. Consider a component that displays the response from an external API when a button is clicked. The hybrid mixin might look like:

const Mixin = (Base, { fetchFn }) => {
static get template() {
return `
<button>Fetch</button>
<h3>Response</h3>
<div id="data"></div>
`
;
}

static get props() {
return {
data: {
type: String,
targets: [{
selector: '$#data',
text: true,
}]
}
}
}

get events() {
return {
button: {
click: this.getData
}
}
}

async getData() {
const response = await fetchFn('https://...');
const json = await response.json();

this.data = json.data;
}
}

By passing in a context dependent fetch function the same code can still be used on the server and client:

// Server
import WaferServer from "@lamplightdev/wafer/lib/server/wafer.js";
import fetch from "node-fetch";

class MyExampleServer extends Mixin(WaferServer, {
// pass in a node based fetch implementation
fetchFn: fetch,
}) {}
// Client
import Wafer from "@lamplightdev/wafer";

class MyExampleClient extends Mixin(Wafer, {
// pass in the browser fetch implementation
fetchFn: fetch,
}) {}

customElements.define("my-example", MyExampleClient);
Next: Server only Previous: Rendering