In my earlier submit we checked out Shoelace, which is a part library with a full suite of UX parts which can be lovely, accessible, and — maybe unexpectedly — constructed with Internet Parts. This implies they can be utilized with any JavaScript framework. Whereas React’s Internet Element interoperability is, at current, lower than perfect, there are workarounds.
However one severe shortcoming of Internet Parts is their present lack of help for server-side rendering (SSR). There’s something referred to as the Declarative Shadow DOM (DSD) within the works, however present help for it’s fairly minimal, and it truly requires buy-in out of your internet server to emit particular markup for the DSD. There’s at the moment work being accomplished for Subsequent.js that I stay up for seeing. However for this submit, we’ll have a look at find out how to handle Internet Parts from any SSR framework, like Subsequent.js, immediately.
We’ll wind up doing a non-trivial quantity of handbook work, and barely hurting our web page’s startup efficiency within the course of. We’ll then have a look at find out how to reduce these efficiency prices. However make no mistake: this answer isn’t with out tradeoffs, so don’t anticipate in any other case. All the time measure and profile.
The issue
Earlier than we dive in, let’s take a second and really clarify the issue. Why don’t Internet Parts work nicely with server-side rendering?
Software frameworks like Subsequent.js take React code and run it via an API to basically “stringify” it, which means it turns your parts into plain HTML. So the React part tree will render on the server internet hosting the net app, and that HTML might be despatched down with the remainder of the net app’s HTML doc to your consumer’s browser. Together with this HTML are some <script> tags that load React, together with the code for all of your React parts. When a browser processes these <script> tags, React will re-render the part tree, and match issues up with the SSR’d HTML that was despatched down. At this level, the entire results will begin working, the occasion handlers will wire up, and the state will truly… comprise state. It’s at this level that the net app turns into interactive. The method of re-processing your part tree on the consumer, and wiring the whole lot up is known as hydration.
So, what does this need to do with Internet Parts? Properly, if you render one thing, say the identical Shoelace <sl-tab-group> part we visited final time:
<sl-tab-group ref=”{tabsRef}”>
<sl-tab slot=”nav” panel=”common”> Common </sl-tab>
<sl-tab slot=”nav” panel=”customized”> Customized </sl-tab>
<sl-tab slot=”nav” panel=”superior”> Superior </sl-tab>
<sl-tab slot=”nav” panel=”disabled” disabled> Disabled </sl-tab>
<sl-tab-panel identify=”common”>That is the overall tab panel.</sl-tab-panel>
<sl-tab-panel identify=”customized”>That is the customized tab panel.</sl-tab-panel>
<sl-tab-panel identify=”superior”>That is the superior tab panel.</sl-tab-panel>
<sl-tab-panel identify=”disabled”>This can be a disabled tab panel.</sl-tab-panel>
</sl-tab-group>
…React (or truthfully any JavaScript framework) will see these tags and easily cross them alongside. React (or Svelte, or Strong) will not be answerable for turning these tags into nicely-formatted tabs. The code for that’s tucked away inside no matter code you could have that defines these Internet Parts. In our case, that code is within the Shoelace library, however the code might be anyplace. What’s necessary is when the code runs.
Usually, the code registering these Internet Parts might be pulled into your software’s regular code by way of a JavaScript import. Meaning this code will wind up in your JavaScript bundle and execute throughout hydration which signifies that, between your consumer first seeing the SSR’d HTML and hydration taking place, these tabs (or any Internet Element for that matter) is not going to render the right content material. Then, when hydration occurs, the correct content material will show, probably inflicting the content material round these Internet Parts to maneuver round and match the correctly formatted content material. This is named a flash of unstyled content material, or FOUC. In idea, you could possibly stick markup in between all of these <sl-tab-xyz> tags to match the completed output, however that is all however inconceivable in observe, particularly for a third-party part library like Shoelace.
Shifting our Internet Element license plate
So the issue is that the code to make Internet Parts do what they should do received’t truly run till hydration happens. For this submit, we’ll have a look at working that code sooner; instantly, in actual fact. We’ll have a look at customized bundling our Internet Element code, and manually including a script on to our doc’s <head> so it runs instantly, and blocks the remainder of the doc till it does. That is usually a horrible factor to do. The entire level of server-side rendering is to not block our web page from processing till our JavaScript has processed. However as soon as accomplished, it signifies that, because the doc is initially rendering our HTML from the server, the Internet Parts might be registered and can each instantly and synchronously emit the correct content material.
In our case, we’re simply seeking to run our Internet Element license plate in a blocking script. This code isn’t big, and we’ll look to considerably reduce the efficiency hit by including some cache headers to assist with subsequent visits. This isn’t an ideal answer. The primary time a consumer browses your web page will all the time block whereas that script file is loaded. Subsequent visits will cache properly, however this tradeoff may not be possible for you — e-commerce, anybody? Anyway, profile, measure, and make the correct resolution on your app. Moreover, sooner or later it’s totally attainable Subsequent.js will totally help DSD and Internet Parts.
Getting began
The entire code we’ll be taking a look at is in this GitHub repo and deployed right here with Vercel. The online app renders some Shoelace parts together with textual content that adjustments colour and content material upon hydration. You must be capable of see the textual content change to “Hydrated,” with the Shoelace parts already rendering correctly.
Customized bundling Internet Element code
Our first step is to create a single JavaScript module that imports all of our Internet Element definitions. For the Shoelace parts I’m utilizing, my code seems to be like this:
import { setDefaultAnimation } from “@shoelace-style/shoelace/dist/utilities/animation-registry”;
import “@shoelace-style/shoelace/dist/parts/tab/tab.js”;
import “@shoelace-style/shoelace/dist/parts/tab-panel/tab-panel.js”;
import “@shoelace-style/shoelace/dist/parts/tab-group/tab-group.js”;
import “@shoelace-style/shoelace/dist/parts/dialog/dialog.js”;
setDefaultAnimation(“dialog.present”, {
keyframes: [
{ opacity: 0, transform: “translate3d(0px, -20px, 0px)” },
{ opacity: 1, transform: “translate3d(0px, 0px, 0px)” },
],
choices: { length: 250, easing: “cubic-bezier(0.785, 0.135, 0.150, 0.860)” },
});
setDefaultAnimation(“dialog.disguise”, {
keyframes: [
{ opacity: 1, transform: “translate3d(0px, 0px, 0px)” },
{ opacity: 0, transform: “translate3d(0px, 20px, 0px)” },
],
choices: { length: 250, easing: “cubic-bezier(0.785, 0.135, 0.150, 0.860)” },
});
It masses the definitions for the <sl-tab-group> and <sl-dialog> parts, and overrides some default animations for the dialog. Easy sufficient. However the attention-grabbing piece right here is getting this code into our software. We can not merely import this module. If we did that, it’d get bundled into our regular JavaScript bundles and run throughout hydration. This is able to trigger the FOUC we’re attempting to keep away from.
Whereas Subsequent.js does have a variety of webpack hooks to customized bundle issues, I’ll use Vite as an alternative. First, set up it with npm i vite after which create a vite.config.js file. Mine seems to be like this:
import { defineConfig } from “vite”;
import path from “path”;
export default defineConfig({
construct: {
outDir: path.be part of(__dirname, “./shoelace-dir”),
lib: {
identify: “shoelace”,
entry: “./src/shoelace-bundle.js”,
codecs: [“umd”],
fileName: () => “shoelace-bundle.js”,
},
rollupOptions: {
output: {
entryFileNames: `[name]-[hash].js`,
},
},
},
});
It will construct a bundle file with our Internet Element definitions within the shoelace-dir folder. Let’s transfer it over to the general public folder in order that Subsequent.js will serve it. And we must also maintain observe of the precise identify of the file, with the hash on the tip of it. Right here’s a Node script that strikes the file and writes a JavaScript module that exports a easy fixed with the identify of the bundle file (it will turn out to be useful shortly):
const fs = require(“fs”);
const path = require(“path”);
const shoelaceOutputPath = path.be part of(course of.cwd(), “shoelace-dir”);
const publicShoelacePath = path.be part of(course of.cwd(), “public”, “shoelace”);
const recordsdata = fs.readdirSync(shoelaceOutputPath);
const shoelaceBundleFile = recordsdata.discover(identify => /^shoelace-bundle/.take a look at(identify));
fs.rmSync(publicShoelacePath, { pressure: true, recursive: true });
fs.mkdirSync(publicShoelacePath, { recursive: true });
fs.renameSync(path.be part of(shoelaceOutputPath, shoelaceBundleFile), path.be part of(publicShoelacePath, shoelaceBundleFile));
fs.rmSync(shoelaceOutputPath, { pressure: true, recursive: true });
fs.writeFileSync(path.be part of(course of.cwd(), “util”, “shoelace-bundle-info.js”), `export const shoelacePath = “/shoelace/${shoelaceBundleFile}”;`);
Right here’s a companion npm script:
“bundle-shoelace”: “vite construct && node util/process-shoelace-bundle”,
That ought to work. For me, util/shoelace-bundle-info.js now exists, and appears like this:
export const shoelacePath = “/shoelace/shoelace-bundle-a6f19317.js”;
Loading the script
Let’s go into the Subsequent.js _document.js file and pull within the identify of our Internet Element bundle file:
import { shoelacePath } from “../util/shoelace-bundle-info”;
Then we manually render a <script> tag within the <head>. Right here’s what my total _document.js file seems to be like:
import { Html, Head, Primary, NextScript } from “subsequent/doc”;
import { shoelacePath } from “../util/shoelace-bundle-info”;
export default operate Doc() {
return (
<Html>
<Head>
<script src={shoelacePath}></script>
</Head>
<physique>
<Primary />
<NextScript />
</physique>
</Html>
);
}
And that ought to work! Our Shoelace registration will load in a blocking script and be out there instantly as our web page processes the preliminary HTML.
Bettering efficiency
We might go away issues as they’re however let’s add caching for our Shoelace bundle. We’ll inform Subsequent.js to make these Shoelace bundles cacheable by including the next entry to our Subsequent.js config file:
async headers() {
return [
{
source: “/shoelace/shoelace-bundle-:hash.js”,
headers: [
{
key: “Cache-Control”,
value: “public,max-age=31536000,immutable”,
},
],
},
];
}
Now, on subsequent browses to our website, we see the Shoelace bundle caching properly!
If our Shoelace bundle ever adjustments, the file identify will change (by way of the :hash portion from the supply property above), the browser will discover that it doesn’t have that file cached, and can merely request it recent from the community.
Wrapping up
This may increasingly have appeared like a number of handbook work; and it was. It’s unlucky Internet Parts don’t provide higher out-of-the-box help for server-side rendering.
However we shouldn’t overlook the advantages they supply: it’s good having the ability to use high quality UX parts that aren’t tied to a particular framework. It’s aldo good having the ability to experiment with model new frameworks, like Strong, without having to seek out (or hack collectively) some type of tab, modal, autocomplete, or no matter part.
Utilizing Internet Parts With Subsequent (or Any SSR Framework) initially revealed on CSS-Methods, which is a part of the DigitalOcean household. You must get the e-newsletter.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!