DEV Community

Viktor Lázár for One Beyond

Posted on • Edited on

React Server Components without any frameworks

Starting with Next.js 13 developers can access the power of React Server Components (let's just use RSC from now on). But if you don't want to use Next.js and you want to start to use RSC out of the box, then how should you start?

Dependencies

You need to use the experimental versions of react, react-dom and react-server-dom-webpack.

Let's start up a new project and install these. We will use pnpm, as it is now the best and most loved choice. Also for quick setup, we will use Vite for the client side and HatTip for our backend handler for more convenient readable stream handling.

mkdir hello-rsc
cd hello-rsc
pnpm init
pnpm add react@experimental react-dom@experimental react-server-dom-webpack@experimental vite @vitejs/plugin-react @hattip/core @hattip/adapter-node
Enter fullscreen mode Exit fullscreen mode

Implementation

To start to work with SSR rendering with Vite, we need to create some files to get started with.

 SSR

We will need an index.html file, which will be our SSR HTML template, which will load the client entry script. This is very-very minimal.

<script type="module" src="index.jsx"></script>
<div id="root"></div>
Enter fullscreen mode Exit fullscreen mode

To start our SSR enabled Vite development server, let's create an index.mjs file, where we will instantiate a Vite development server in middleware mode and with "ssr" as application type.

import { createMiddleware } from "@hattip/adapter-node";
import { createServer as createViteDevServer } from "vite";
import react from "@vitejs/plugin-react";
import { readFile } from "node:fs/promises";

const viteDevServer = await createViteDevServer({
  server: {
    middlewareMode: true,
  },
  appType: "ssr",
  plugins: [react()],
});
Enter fullscreen mode Exit fullscreen mode

We need to preload our index.html for later usage. So let's read up the file as it is.

const html = await readFile("./index.html", "utf-8");
Enter fullscreen mode Exit fullscreen mode

The following HatTip handler will be our router to handle requests which accepts an RSC response. If the Accept header includes the text/x-component MIME type, then we know that the client needs the response in RSC format.

The RSC format is a JSON-like format which includes all the React elements the client will render.

0:"$L1"
1:[["$","h1",null,{"children":"Hello World!"}],["$","h2",null,{"children":["Darwin"," ","x64"," ","22.5.0"]}],"2023-07-07T12:40:30.880Z"]
Enter fullscreen mode Exit fullscreen mode

Our implementation loads the rsc.jsx SSR module using Vite and uses the render function which will render the RSC into a readable stream. We also do some basic error handling.

const ssr = createMiddleware(async ({ request }) => {
  if (request.headers.get("accept").includes("text/x-component")) {
    try {
      const { render } = await viteDevServer.ssrLoadModule("./rsc.jsx");
      return new Response(await render(), {
        headers: {
          "content-type": "text/x-component",
        },
      });
    } catch (e) {
      return new Response(e.stack, {
        status: 500,
        headers: {
          "content-type": "text/plain",
        },
      });
    }
  }
  return new Response(
    await viteDevServer.transformIndexHtml(request.url, html),
    {
      headers: {
        "content-type": "text/html",
      },
    }
  );
});
Enter fullscreen mode Exit fullscreen mode

At the end of this, we only need to start listening to requests using the Vite development server. As the

viteDevServer.middlewares.use(ssr);
viteDevServer.middlewares.listen(3000).on("listening", () => {
  console.log("Listening on http://localhost:3000");
});
Enter fullscreen mode Exit fullscreen mode

React Server Component

How the rsc.jsx looks like? It's really very simple. The main imported module we need to watch out for is the react-server-dom-webpack/server.edge. Our <App /> component is an async function, so we can use await inside of it and at the end, just return JSX where we can also include server-side only functionality, so we give the client information about our OS and the current time. But first, we delay the response for a bit.

import { renderToReadableStream } from "react-server-dom-webpack/server.edge";
import * as OS from "node:os";

async function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function App() {
  await delay(500);
  return (
    <>
      <h1>Hello World!</h1>
      <h2>
        {OS.type()} {OS.arch()} {OS.release()}
      </h2>
      {new Date().toISOString()}
    </>
  );
}

export async function render() {
  return renderToReadableStream(<App />);
}
Enter fullscreen mode Exit fullscreen mode

The exported render function is the one we used in the index.mjs handler, loaded with Vite as an SSR module.

Client

The last part we need to implement is our client entry, where we create our React root to render the RSC response.

import { Suspense } from "react";
import { createRoot } from "react-dom/client";
import { createFromFetch } from "react-server-dom-webpack/client.browser";

let rsc = null;
function App() {
  if (!rsc) {
    rsc = createFromFetch(
      fetch("/app", {
        headers: {
          Accept: "text/x-component",
        },
      })
    );
  }

  return rsc;
}

const root = createRoot(document.getElementById("root"));
root.render(
  <Suspense fallback="Loading...">
    <App />
  </Suspense>
);
Enter fullscreen mode Exit fullscreen mode

We need to add a Suspense around our <App /> component so it will show a fallback loading message while the client receives the RSC response.

We also need to "cache" the result of createFromFetch as we don't want this to be fired more than once and it always should be the same, so React can handle the <App /> component properly.

Future enhancement

Currently, our RSC rendering is not supporting client components ('use client'), only server-side rendered elements. We also don't support server actions in this implementation. These are much more advanced topics and you should use Next.js 13 for this or another meta-framework supporting these features.

Summary

Surely, this is only a "Hello World!" level implementation and could be enhanced, but maybe this is a good starting point when you want to start playing with some low-level implementation regarding React Server Components.

You can access the code at this repo

Thanks for reading, please click on follow and have a nice day!

Top comments (1)

Collapse
 
javiasilis profile image
Jose

Oh pretty nice. Thanks for sharing. There seems to be a lot of work involved in server components. I'm wondering how much work will it require to port to non-js backends.