React Server Components without any frameworks
Tags: react
Published: 2023-07-07 15:21:00

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](https://vitejs.dev) for the client side and [HatTip](https://github.com/hattipjs/hattip) for our backend handler for more convenient readable stream handling.

```sh
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
```

# 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.

```html
<script type="module" src="index.jsx"></script>
<div id="root"></div>
```

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.

```js
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()],
});
```

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

```js
const html = await readFile("./index.html", "utf-8");
```

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"]
```

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.

```js
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",
      },
    }
  );
});
```

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

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

### 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.

```jsx
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 />);
}
```

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.

```jsx
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>
);
```

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](https://github.com/lazarv/hello-rsc)

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