Friday, March 29, 2024
HomeProgrammingUtilizing Internet Parts With Subsequent (or Any SSR Framework) | CSS-Methods

Utilizing Internet Parts With Subsequent (or Any SSR Framework) | CSS-Methods


In my earlier publish we checked out Shoelace, which is a part library with a full suite of UX parts which might be lovely, accessible, and — maybe unexpectedly — constructed with Internet Parts. This implies they can be utilized with any JavaScript framework. Whereas React’s Internet Part interoperability is, at current, lower than best, there are workarounds.

However one critical shortcoming of Internet Parts is their present lack of assist for server-side rendering (SSR). There’s something known as the Declarative Shadow DOM (DSD) within the works, however present assist for it’s fairly minimal, and it really requires buy-in out of your net server to emit particular markup for the DSD. There’s at the moment work being executed for Subsequent.js that I sit up for seeing. However for this publish, we’ll have a look at handle Internet Parts from any SSR framework, like Subsequent.js, right now.

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 decrease these efficiency prices. However make no mistake: this resolution just isn’t with out tradeoffs, so don’t count on in any other case. All the time measure and profile.

The issue

Earlier than we dive in, let’s take a second and truly clarify the issue. Why don’t Internet Parts work properly with server-side rendering?

Software frameworks like Subsequent.js take React code and run it by way of an API to primarily “stringify” it, that 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 will likely 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, all the results will begin operating, the occasion handlers will wire up, and the state will really… comprise state. It’s at this level that the net app turns into interactive. The method of re-processing your part tree on the shopper, and wiring every little thing up known as hydration.

So, what does this should do with Internet Parts? Properly, once 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="normal"> Normal </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="normal">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">It is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>

…React (or actually any JavaScript framework) will see these tags and easily move them alongside. React (or Svelte, or Stable) will not be answerable for turning these tags into nicely-formatted tabs. The code for that’s tucked away within no matter code you’ve got that defines these Internet Parts. In our case, that code is within the Shoelace library, however the code may be wherever. What’s essential is when the code runs.

Usually, the code registering these Internet Parts will likely 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 implies that, between your consumer first seeing the SSR’d HTML and hydration taking place, these tabs (or any Internet Part for that matter) won’t render the right content material. Then, when hydration occurs, the right content material will show, doubtless inflicting the content material round these Internet Parts to maneuver round and match the correctly formatted content material. This is called a flash of unstyled content material, or FOUC. In idea, you can stick markup in between all of these <sl-tab-xyz> tags to match the completed output, however that is all however unimaginable in observe, particularly for a third-party part library like Shoelace.

Shifting our Internet Part license plate

So the issue is that the code to make Internet Parts do what they should do gained’t really run till hydration happens. For this publish, we’ll have a look at operating that code sooner; instantly, in actual fact. We’ll have a look at customized bundling our Internet Part 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 executed, it implies that, because the doc is initially rendering our HTML from the server, the Internet Parts will likely be registered and can each instantly and synchronously emit the appropriate content material.

In our case, we’re simply trying to run our Internet Part 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 resolution. 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 won’t be possible for you — e-commerce, anybody? Anyway, profile, measure, and make the appropriate resolution to your app. In addition to, sooner or later it’s fully potential Subsequent.js will totally assist DSD and Internet Parts.

Getting began

All the code we’ll be is in this GitHub repo and deployed right here with Vercel. The online app renders some Shoelace parts together with textual content that adjustments coloration and content material upon hydration. You need to be capable of see the textual content change to “Hydrated,” with the Shoelace parts already rendering correctly.

Customized bundling Internet Part code

Our first step is to create a single JavaScript module that imports all of our Internet Part definitions. For the Shoelace parts I’m utilizing, my code appears to be like 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.cover", {
  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 hundreds the definitions for the <sl-tab-group> and <sl-dialog> parts, and overrides some default animations for the dialog. Easy sufficient. However the fascinating 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 may trigger the FOUC we’re making an attempt 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 appears to be like like this:

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  construct: {
    outDir: path.be a 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`,
      },
    },
  },
});

This can construct a bundle file with our Internet Part definitions within the shoelace-dir folder. Let’s transfer it over to the public folder in order that Subsequent.js will serve it. And we must also preserve 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 (this can turn out to be useful shortly):

const fs = require("fs");
const path = require("path");

const shoelaceOutputPath = path.be a part of(course of.cwd(), "shoelace-dir");
const publicShoelacePath = path.be a part of(course of.cwd(), "public", "shoelace");

const information = fs.readdirSync(shoelaceOutputPath);

const shoelaceBundleFile = information.discover(identify => /^shoelace-bundle/.take a look at(identify));

fs.rmSync(publicShoelacePath, { drive: true, recursive: true });

fs.mkdirSync(publicShoelacePath, { recursive: true });
fs.renameSync(path.be a part of(shoelaceOutputPath, shoelaceBundleFile), path.be a part of(publicShoelacePath, shoelaceBundleFile));
fs.rmSync(shoelaceOutputPath, { drive: true, recursive: true });

fs.writeFileSync(path.be a 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 Part bundle file:

import { shoelacePath } from "../util/shoelace-bundle-info";

Then we manually render a <script> tag within the <head>. Right here’s what my whole _document.js file appears to be like like:

import { Html, Head, Most important, NextScript } from "subsequent/doc";
import { shoelacePath } from "../util/shoelace-bundle-info";

export default perform Doc() {
  return (
    <Html>
      <Head>
        <script src={shoelacePath}></script>
      </Head>
      <physique>
        <Most important />
        <NextScript />
      </physique>
    </Html>
  );
}

And that ought to work! Our Shoelace registration will load in a blocking script and be accessible instantly as our web page processes the preliminary HTML.

Bettering efficiency

We may 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 web site, we see the Shoelace bundle caching properly!

DevTools Sources panel open and showing the loaded Shoelace bundle.

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 will have appeared like plenty of handbook work; and it was. It’s unlucky Internet Parts don’t supply higher out-of-the-box assist 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 Stable, while not having to search out (or hack collectively) some form of tab, modal, autocomplete, or no matter part.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments