@vitejs/plugin-rsc
This package provides React Server Components (RSC) support for Vite.
Features
- Framework-agnostic: The plugin implements RSC bundler features and provides low level RSC runtime (
react-server-dom
) API without framework-specific abstractions.
- Runtime-agnostic: Built on Vite environment API and works with other runtimes (e.g.,
@cloudflare/vite-plugin
).
- HMR support: Enables editing both client and server components without full page reloads.
- CSS support: CSS is automatically code-split both at client and server components and they are injected upon rendering.
Getting Started
You can create a starter project by:
npm create vite@latest -- --template rsc
Examples
Start here: ./examples/starter
- Recommended for understanding the package
- Provides an in-depth overview of API with inline comments to explain how they function within RSC-powered React application.
Integration examples:
./examples/basic
- Advanced RSC features and testing
- This is mainly used for e2e testing and includes various advanced RSC usages (e.g.
"use cache"
example).
./examples/ssg
- Static site generation with MDX and client components for interactivity.
./examples/react-router
- React Router RSC integration
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"] --> |"[@vitejs/plugin-rsc/rsc]<br /><code>renderToReadableStream</code>"| B1["RSC Stream"];
end
B1 --> B2
B1 --> B3
subgraph "<strong>ssr environment</strong>"
B2["RSC Stream"] --> |"[@vitejs/plugin-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"] --> |"[@vitejs/plugin-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 '@vitejs/plugin-rsc'
import { defineConfig } from 'vite'
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 { renderToReadableStream } from '@vitejs/plugin-rsc/rsc'
export default async function handler(request: Request): Promise<Response> {
const root = (
<html>
<body>
<h1>Test</h1>
</body>
</html>
)
const rscStream = renderToReadableStream(root)
if (request.url.endsWith('.rsc')) {
return new Response(rscStream, {
headers: {
'Content-type': 'text/x-component;charset=utf-8',
},
})
}
const ssrEntry = await import.meta.viteRsc.loadModule<
typeof import('./entry.ssr.tsx')
>('ssr', 'index')
const htmlStream = await ssrEntry.handleSsr(rscStream)
return new Response(htmlStream, {
headers: {
'Content-type': 'text/html',
},
})
}
if (import.meta.hot) {
import.meta.hot.accept()
}
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
import { renderToReadableStream } from 'react-dom/server.edge'
export async function handleSsr(rscStream: ReadableStream) {
const root = await createFromReadableStream(rscStream)
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index')
const htmlStream = renderToReadableStream(root, {
bootstrapScriptContent,
})
return htmlStream
}
import { createFromReadableStream } from '@vitejs/plugin-rsc/browser'
import { hydrateRoot } from 'react-dom/client'
async function main() {
const rscResponse = await fetch(window.location.href + '.rsc')
const root = await createFromReadableStream(rscResponse.body)
hydrateRoot(document, root)
}
main()
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
[!NOTE]
The plugin automatically injects CSS for server components. See the CSS Support section for detailed information about automatic CSS injection.
- 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()}
...
</>
)
}
When 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>
}
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 { 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 { createFromFetch } from '@vitejs/plugin-rsc/browser'
import.meta.hot.on('rsc:update', async () => {
const rscPayload = await createFromFetch(fetch(window.location.href + '.rsc'))
})
Plugin API
@vitejs/plugin-rsc
- Type:
rsc: (options?: RscPluginOptions) => Plugin[]
;
import rsc from '@vitejs/plugin-rsc'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
rsc({
entries: {
rsc: '...',
ssr: '...',
client: '...',
},
serverHandler: false,
validateImports: true,
defineEncryptionKey: 'process.env.MY_ENCRYPTION_KEY',
loadModuleDevProxy: true,
rscCssTransform: { filter: (id) => id.includes('/my-app/') },
}),
],
rsc: {
},
})
RSC runtime (react-server-dom) API
@vitejs/plugin-rsc/rsc
This module re-exports RSC runtime API provided by react-server-dom/server.edge
and react-server-dom/client.edge
such as:
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 serialized RSC and deserializing it for later use.
decodeAction/decodeReply/decodeFormState/loadServerAction/createTemporaryReferenceSet
encodeReply/createClientTemporaryReferenceSet
@vitejs/plugin-rsc/ssr
This module re-exports RSC runtime API provided by react-server-dom/client.edge
createFromReadableStream
: RSC deserialization (RSC stream -> React VDOM)
@vitejs/plugin-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...
Tips
CSS Support
The plugin automatically handles CSS code-splitting and injection for server components. This eliminates the need to manually call import.meta.viteRsc.loadCss()
in most cases.
-
Component Detection: The plugin automatically detects server components by looking for:
- Function exports with capital letter names (e.g.,
export function Page() {}
)
- Default exports that are functions with capital names (e.g.,
export default function Page() {}
)
- Const exports assigned to functions with capital names (e.g.,
export const Page = () => {}
)
-
CSS Import Detection: For detected components, the plugin checks if the module imports any CSS files (.css
, .scss
, .sass
, etc.)
-
Automatic Wrapping: When both conditions are met, the plugin wraps the component with a CSS injection wrapper:
import './styles.css'
export function Page() {
return <div>Hello</div>
}
import './styles.css'
export function Page() {
return (
<>
{import.meta.viteRsc.loadCss()}
<div>Hello</div>
</>
)
}
Canary and Experimental channel releases
See https://github.com/vitejs/vite-plugin-react/pull/524 for how to install the package for React canary and experimental usages.
Using @vitejs/plugin-rsc
as a framework package's dependencies
By default, @vitejs/plugin-rsc
is expected to be used as peerDependencies
similar to react
and react-dom
. When @vitejs/plugin-rsc
is not available at the project root (e.g., in node_modules/@vitejs/plugin-rsc
), you will see warnings like:
Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/client.browser, present in client 'optimizeDeps.include'
This can be fixed by updating optimizeDeps.include
to reference @vitejs/plugin-rsc
through your framework package. For example, you can add the following plugin:
export default function myRscFrameworkPlugin() {
return {
name: 'my-rsc-framework:config',
configEnvironment(_name, config) {
if (config.optimizeDeps?.include) {
config.optimizeDeps.include = config.optimizeDeps.include.map(
(entry) => {
if (entry.startsWith('@vitejs/plugin-rsc')) {
entry = `my-rsc-framework > ${entry}`
}
return entry
},
)
}
},
}
}
Typescript
Types for global API are defined in @vitejs/plugin-rsc/types
. For example, you can add it to tsconfig.json
to have types for import.meta.viteRsc
APIs:
{
"compilerOptions": {
"types": ["vite/client", "@vitejs/plugin-rsc/types"]
}
}
import.meta.viteRsc.loadModule
See also Vite documentation for vite/client
types.
server-only
and client-only
import
You can use the server-only
import to prevent accidentally importing server-only code into client bundles, which can expose sensitive server code in public static assets.
For example, the plugin will show an error 'server-only' cannot be imported in client build
for the following code:
import 'server-only'
export async function getData() {
const res = await fetch('https://internal-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
'use client'
import { getData } from './server-utils.js'
...
Similarly, the client-only
import ensures browser-specific code isn't accidentally imported into server environments.
For example, the plugin will show an error 'client-only' cannot be imported in server build
for the following code:
import 'client-only'
export function getStorage(key) {
return window.localStorage.getItem(key)
}
import { getStorage } from './client-utils.js'
export function ServerComponent() {
const data = getStorage("settings")
...
}
Note that while there are official npm packages server-only
and client-only
created by React team, they don't need to be installed. The plugin internally overrides these imports and surfaces their runtime errors as build-time errors.
This build-time validation is enabled by default and can be disabled by setting validateImports: false
in the plugin options.
Credits
This project builds on fundamental techniques and insights from pioneering Vite RSC implementations.
Additionally, Parcel and React Router's work on standardizing the RSC bundler/app responsibility has guided this plugin's API design: