@hiogawa/vite-rsc
This package provides React Server Components (RSC) support for Vite.
Any feedback is welcome, please feel free to share an idea in the discussion.
Features
- Framework-less RSC experience: The plugin implements RSC conventions and provides low level
react-server-dom runtime API without framework-specific abstractions.
- CSS support: CSS is automatically code-split both at client and server components and they are injected upon rendering.
- HMR support: Enables editing both client and server components without full page reloads.
- Runtime agnostic: Built on Vite environment API and works with other runtimes (e.g.,
@cloudflare/vite-plugin).
Getting Started
You can start a project by copying an example locally by:
npx degit hi-ogawa/vite-plugins/packages/rsc/examples/starter my-app
Examples
Basic Concepts
This example is a simplified version of ./examples/starter. You can read ./examples/starter/src/framework/entry.{rsc,ssr,browser}.tsx for more in-depth commentary, which includes server function handling and client-side RSC re-fetching/re-rendering.
This is the diagram to show the basic flow of RSC rendering process. See also https://github.com/hi-ogawa/vite-plugins/discussions/606.
graph TD
subgraph "<strong>rsc environment</strong>"
A["React virtual dom tree"] --> |"[@hiogawa/vite-rsc/rsc]<br /><code>renderToReadableStream</code>"| B1["RSC Stream"];
end
B1 --> B2
B1 --> B3
subgraph "<strong>ssr environment</strong>"
B2["RSC Stream"] --> |"[@hiogawa/vite-rsc/ssr]<br /><code>createFromReadableStream</code>"| C1["React virtual dom tree"];
C1 --> |"[react-dom/server]<br/>SSR"| E["HTML String/Stream"];
end
subgraph "<strong>client environment</strong>"
B3["RSC Stream"] --> |"[@hiogawa/vite-rsc/browser]<br /><code>createFromReadableStream</code>"| C2["React virtual dom tree"];
C2 --> |"[react-dom/client]<br/>CSR: mount, hydration"| D["DOM Elements"];
end
style A fill:#D6EAF8,stroke:#333,stroke-width:2px
style B1 fill:#FEF9E7,stroke:#333,stroke-width:2px
style B2 fill:#FEF9E7,stroke:#333,stroke-width:2px
style B3 fill:#FEF9E7,stroke:#333,stroke-width:2px
style C1 fill:#D6EAF8,stroke:#333,stroke-width:2px
style C2 fill:#D6EAF8,stroke:#333,stroke-width:2px
style D fill:#D5F5E3,stroke:#333,stroke-width:2px
style E fill:#FADBD8,stroke:#333,stroke-width:2px
import rsc from "@hiogawa/vite-rsc/plugin";
export default defineConfig() {
plugins: [
rsc(),
],
environments: {
rsc: {
build: {
rollupOptions: {
input: {
index: "./src/framework/entry.rsc.tsx",
},
},
},
},
ssr: {
build: {
rollupOptions: {
input: {
index: "./src/framework/entry.ssr.tsx",
},
},
},
},
client: {
build: {
rollupOptions: {
input: {
index: "./src/framework/entry.browser.tsx",
},
},
},
},
},
}
import * as ReactServer from "@hiogawa/vite-rsc/rsc";
export default async function handler(request: Request): Promise<Response> {
const root = <html><body><h1>Test</h1></body></html>;
const rscStream = ReactServer.renderToReadableStream(root);
if (request.url.endsWith(".rsc")) {
return new Response(rscStream, {
headers: {
'Content-type': 'text/html'
}
})
}
const ssrEntry = await import.meta.viteRsc.loadSsrModule<typeof import("./entry.ssr.tsx")>();
const htmlStream = await ssrEntry.handleSsr(rscStream);
return new Response(htmlStream, {
headers: {
'Content-type': 'text/html'
}
})
}
import * as ReactClient from "@hiogawa/vite-rsc/ssr";
import * as ReactDOMServer from "react-dom/server.edge";
export async function handleSsr(rscStream: ReadableStream) {
const root = await ReactClient.createFromReadableStream(rscStream);
const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index");
const htmlStream = ReactDOMServer.renderToReadableStream(root, {
bootstrapScriptContent,
})
return htmlStream;
}
import * as ReactClient from "@hiogawa/vite-rsc/browser";
import * as ReactDOMClient from "react-dom/client";
async function main() {
const rscResponse = await fetch(window.location.href + ".rsc");
const root = await ReactClient.createFromReadableStream(rscResponse.body);
ReactDOMClient.hydrateRoot(document, root);
}
main();
react-server-dom API
@hiogawa/vite-rsc/rsc
This module re-exports RSC runtime API provided by react-server-dom/server.edge
renderToReadableStream: RSC serialization (React VDOM -> RSC stream)
createFromReadableStream: RSC deserialization (RSC stream -> React VDOM). This is also available on rsc environment itself. For example, it allows saving serailized RSC and deserializing it for later use.
decodeAction/decodeReply/loadServerAction: server function related...
@hiogawa/vite-rsc/ssr
This module re-exports RSC runtime API provided by react-server-dom/client.edge
createFromReadableStream: RSC deserialization (RSC stream -> React VDOM)
@hiogawa/vite-rsc/browser
This module re-exports RSC runtime API provided by react-server-dom/client.browser
createFromReadableStream: RSC deserialization (RSC stream -> React VDOM)
createFromFetch: a robust way of createFromReadableStream((await fetch("...")).body)
encodeReply/setServerCallback: server function related...
Environment helper API
The plugin provides an additional helper for multi environment interaction.
available on rsc or ssr environment
import.meta.viteRsc.loadModule
- Type:
(environmentName: "ssr" | "rsc", entryName: string) => Promise<T>
This allows importing ssr environment module specified by environments.ssr.build.rollupOptions.input[entryName] inside rsc environment and vice versa.
During development, by default, this API assumes both rsc and ssr environments execute under the main Vite process. When enabling rsc({ loadModuleDevProxy: true }) plugin option, the loaded module is implemented as a proxy with fetch-based RPC to call in node environment on the main Vite process, which for example, allows rsc environment inside cloudflare workers to access ssr environment on the main Vite process.
During production build, this API will be rewritten into a static import of the specified entry of other environment build and the modules are executed inside the same runtime.
For example,
const ssrModule = await import.meta.viteRsc.loadModule("ssr", "index");
ssrModule.renderHTML(...);
export function renderHTML(...) {}
available on rsc environment
import.meta.viteRsc.loadCss
- Type:
(importer?: string) => React.ReactNode
This allows collecting css which is imported through a current server module and injecting them inside server components.
import "./test.css";
import dep from "./dep.tsx";
export function ServerPage() {
return <>
{import.meta.viteRsc.loadCss()}
...
</>
}
Where specifying loadCss(<id>), it will collect css through the server module resolved by <id>.
export function Assets() {
return <>
{import.meta.viteRsc.loadCss("/routes/home.tsx")}
{import.meta.viteRsc.loadCss("/routes/about.tsx")}
{...}
</>
}
import { Assets } from "virtual:my-framework-helper";
export function UserApp() {
return <html>
<head>
<Assets />
</head>
<body>...</body>
</html>
}
<id>?vite-rsc-css-export=<name>
This special query convention provides automatic injection of import.meta.viteRsc.loadCss.
For example,
export function Page(props) {
return <div>...</div>
}
function Page(props) {
return <div>...</div>
}
function __Page(props) {
return <>
{import.meta.viteRsc.loadCss()}
<Page {...props} />
</>
}
export { __Page as Page }
Underlying transform utility is available from @hiogawa/vite-rsc/plugin:
import { transformRscCssExport } from "@hiogawa/vite-rsc/plugin";
available on ssr environment
import.meta.viteRsc.loadBootstrapScriptContent("index")
This provides a raw js code to execute a browser entry file specified by environments.client.build.rollupOptions.input.index. This is intended to be used with React DOM SSR API, such as renderToReadableStream
import bootstrapScriptContent from "virtual:vite-rsc/bootstrap-script-content"
import { renderToReadableStream } from "react-dom/server.edge";
const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index")
const htmlStream = await renderToReadableStream(reactNode, { bootstrapScriptContent });
available on client environment
rsc:update event
This event is fired when server modules are updated, which can be used to trigger re-fetching and re-rendering of RSC components on browser.
import * as ReactClient from "@hiogawa/vite-rsc/browser";
import.meta.hot.on("rsc:update", async () => {
const rscPayload = await ReactClient.createFromFetch(fetch(window.location.href + ".rsc"))
});
Plugin API
@hiogawa/vite-rsc/plugin
import rsc from "@hiogawa/vite-rsc/plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
rsc({
entries: {
rsc: "...",
ssr: "...",
client: "...",
},
serverHandler: false,
loadModuleDevProxy: true,
rscCssTransform: { filter: id => id.includes("/my-app/") },
defineEncryptionKey: "process.env.MY_ENCRYPTION_KEY",
}),
],
});
Higher level API
This is a wrapper of react-server-dom API and helper API to setup a minimal RSC app without writing own framework code like ./examples/starter/src/framework. See ./examples/basic for how this API is used.