Skip to content

nicobrinkkemper/vite-plugin-react-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vite React Server Plugin

A Vite plugin that enables React Server Components (RSC) streaming and static HTML page generation. It leverages experimental dependencies from React, specifically react-server-dom-esm.

Example Projects

Installation

npm install -D vite-plugin-react-server

Open Source and Work in Progress

This project uses the latest OSS-experimental React version from the official React GitHub repository. The plugin includes a patch system to facilitate setup. First, install dependencies and patches:

npm install -D patch-package react@experimental react-dom@experimental react-server-dom-esm

Add the following command to your package.json scripts:

"patch": "patch"

Run the patch command:

npm run patch

It will instruct you to add:

"postinstall": "patch-package"

This ensures the patch is applied after every npm install. If errors arise related to react-server-dom-esm, verify that the postinstall step ran.


Plugin Structure and Purpose

Environment-Based Execution

This plugin uses environment detection to determine the execution context. It achieves this by checking the NODE_OPTIONS environment variable:

import { getCondition } from "vite-plugin-react-server/config";

if (getCondition() !== "react-server") {
  throw new Error("-10 poision damage");
}

Alternatively, you can pass the argument for the react- prefix to just get client or server back.

import { getCondition } from "vite-plugin-react-server/config";

import(`plugin.${getCondition("")}.js`);

The main entry point adapts based on the environment:

  • Client Mode (default) → Does not require the react-server condition, uses a worker thread for RSC requests Benefits:
    • log errors to console
    • onMetric event for each page
    • worker thread
  • Server Mode (NODE_OPTIONS="--conditions react-server") → Does not need worker thread for RSC requests
    • Direct pipeline from vite to react

Custom composition

You can pick and choose only the plugins you like to get the desired behavior as well. For example, we can choose only to use the preserver, the transformer, static plugin, etc.

Page & prop setup

The minimal config is

// vite.config.tsx
import type { StreamPluginOptions } from "vite-plugin-react-server/types";
import { join } from "node:path";
import { defineConfig } from "vite";
import { vitePluginReactServer } from "vite-plugin-react-server";
import { config } from "./vite.react.config.js";

export default defineConfig(() => {
  return {
    plugins: vitePluginReactServer({
      moduleBase: "src",
      Page: "src/page.tsx",
    }),
  };
});

And our Page file.

// src/page.tsx
import React from "react";
export function Page({ url }) {
  return <div>You are on {url}</div>;
}

Of course we need a client file as well, and the vite index.html pointing to it,

import React, { use } from "react";
import { createRoot } from "react-dom/client";
import { createReactFetcher } from "vite-plugin-react-server/utils";
// src/client.tsx
const Shell: React.FC<{
  data: React.Usable<React.ReactNode>;
}> = ({ data: initialServerData }) => {
  const content = use(initialServerData);
  return content as React.ReactNode;
};
// Initialize the app
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");

const intitalData = createReactFetcher({
  url: window.location.pathname,
  moduleBaseURL: import.meta.env.BASE_URL,
  publicOrigin: import.meta.env.PUBLIC_ORIGIN,
});

createRoot(rootElement).render(<Shell data={intitalData} />);

index.html for completeness sake

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <body>
    <div id="root"></div>
    <script type="module" src="/src/client.tsx"></script>
  </body>
</html>

By default, without any prop configurations, the Page receives a normalized url.

With custom "get prop" function, we can enrich the props with more information.

import React from "react";

export const props = (url) => ({ title: "Hello World", file: import.meta.url, url });

export type Props = ReturnType<typeof props>

export function Page({ file, title, url }: Props) {
  return <>
     <title>{title}<title>
     <div>This file is here: {file}</div>;
     <div>You are on: {url}</div>;
   </>
}

You can also define a router specifically for the props file.

{
  moduleBase: "src",
  Page: "src/page.tsx",
  // define the props router
  props: "src/props.ts",
}

Move prop lines from src/page.tsx to src/props.ts

export const props = (url) => ({
  title: "Hello World",
  file: import.meta.url,
  url,
});

export type Props = ReturnType<typeof props>;

We can also make a static build for these pages, which will render them to index.html and headless index.rsc files, which can be used to make a static RSC site.

{
  moduleBase: "src",
  Page: "src/page.tsx",
  props: "src/props.ts"
  // define the routes we want to render
  build: {
    pages: ['/', '/404']
  }
};

And that's how you can work with react server components using a familiar vite workflow. If your app grows and you need more control, see the docs - check out the source code - and have fun building.

Worker support

The client plugin uses the rsc-worker to create server side streams. The server plugin uses the html-worker to create client side html. If you don't want to use the rsc-worker, simply don't serve the plugin without the react-server condition. If you don't want to use the html-worker simply don't configure the build.pages option.

Custom Worker

Both workers can be customized using the htmlWorkerPath and rscWorkerPath respectively. The paths will be used to create the workers instead of the prebuilt worker included with this plugin. If these paths are defined, they will be made part of your application build as well.

Keep in mind that, using your custom worker means interacting with the message system of this plugin during development/static generation process.

Plugin Usage

import { defineConfig, type Plugin } from "vite";
import { vitePluginReactClient } from "vite-plugin-react-server";
import { config } from "./vite.react.config";
import type { StreamPluginOptions } from "vite-plugin-react-server/server";

const createRouter = (file: "props.ts" | "page.tsx") => (url: string) => {
  switch (url) {
    case "/":
      return `src/page/${file}`;
    case "/bidoof":
      return `src/page/bidoof/${file}`;
    case "/404":
    default:
      return `src/page/404/${file}`;
  }
};

export const config = {
  moduleBase: "src",
  Page: createRouter("page.tsx"),
  props: createRouter("props.ts"),
  Html: Html,
  build: {
    pages: ["/", "/bidoof", "/404"],
  },
} satisfies StreamPluginOptions;

export default defineConfig({
  plugins: vitePluginReactClient(config),
});

This will mirror your directory structure for new static routes. If you need to handle dynamic requests, like pointing /:theme/ to a certain folder, you need to parse this yourself using code.

Async build pages

If you have a large amount of pages that needs async operations to fetch, you can pass a async function to build pages.

build:{
  pages: async ()=>await import('my-pages')
}

Built-in React Server Components

This plugin built-in React Component that can be configured through the options to be your own component. Direct server component config inputs are not yet supported through worker threads.

  • Html - used as the wrapper for production pages (use vite's index.html for the development wrapper and entry point for client files & global css)
  • CssCollector - used to emit <link> and <style> tags based on css config

Defining your custom Html React server component will affect the final production output.

Build Steps

vite build

Targets browsers, outputs to dist/static.

vite build --ssr

Targets non-react-server node environment, used for server-side-rendering, outputs to dist/client.

NODE_OPTIONS="--conditions=react-server" vite build

Targets react-server-only environment, outputs to dist/server. In this case, ssr is implied and defaults to true.


vite-plugin-react-server

import { defineConfig, Plugin } from "vite";
import { vitePluginReactServer } from "vite-plugin-react-server";
import { config } from "./vite.react.config";

export default defineConfig({
  plugins: vitePluginReactServer(config),
});

Running in Development

NODE_OPTIONS="--conditions=react-server" vite

A direct server pipeline that doesn't require a rsc-worker.

To develop the app using the rsc-worker, simply run

vite

without the react-server condition. This will work a little bit differently under the hood, it can provide additional development support like error logging, metric events and custom rsc worker development.

Static Site Generation

Single-out the static generation step by only inluding the static plugin. Expects client and server folders to be there.

import { defineConfig, Plugin } from "vite";
import { reactStaticPlugin } from "vite-plugin-react-server/static";
import { config } from "./vite.react.config";

export default defineConfig({
  plugins: [reactStaticPlugin(config)],
});

Example output structure:

dist/static/index.html
dist/static/index.rsc
dist/static/about/index.html
dist/static/about/index.rsc

This plugin is included by default when the react-server condition is set.


Configuration

moduleBase

const config = {
  moduleBase: "src",
};

Defines the root directory for project modules. This can be customized.

moduleBasePath

moduleBasePath: "/",

Passed as the second argument to renderToPipeableStream for server-side rendering.

moduleBaseURL

moduleBaseURL: "/",

Defines asset URL resolution for CSS collectors and bootstrapModule.

publicOrigin: "https://github.com",

Page and props Mapping

Page: (id) => join('src', id, "page.tsx");

Defines how pages are mapped to file paths.

props: (id) => join('src', id, "props.ts");

Defines how to load the initial props of the page file.

If you do not want prop files, just don't define it.

pageExportName: 'Page',

Changes the default name "Page"

propsExportName: 'props',

Changes the default name "props"


Example Setup

package.json Scripts

"scripts": {
  "build": "build:static && build:client && build:server",
  "dev": "NODE_OPTIONS='--conditions react-server' vite",
  "start": "vite",
  "build:server": "NODE_OPTIONS='--conditions react-server' vite build",
  "build:client": "vite build --ssr",
  "build:static": "vite build"
}

Sample Page Component

// src/my-page.tsx
export const Page = ({ name }) => {
  return <div>Hello {name}</div>;
};
// src/async-page.tsx
export const Page = async ({ name }) => {
  return <div>Hello {name}</div>;
};

Sample Props File

All of the below are valid

// src/my-props.ts
export const props = {
  name: "John Doe",
};
export const props = (url)=>{
  name: "John Doe",
};
export const props = async (url)=>{
  name: "John Doe",
}
// enum bonus
export const props = ['key']; // -> {key: "key"}
// Object.fromEntries()
export const props = [['key',{value: 'some value'}]]

Contributions

If you want to help develop or maintain the plugin feel free to open a PR or issue on GitHub.

About

Vite plugin for React Server Components (RSC)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published