# @vitejs/plugin-rsc
This package provides [React Server Components](https://react.dev/reference/rsc/server-components) (RSC) support for Vite.
## Features
- **Framework-agnostic**: The plugin implements [RSC bundler features](https://react.dev/reference/rsc/server-components) and provides low level RSC runtime (`react-server-dom`) API without framework-specific abstractions.
- **Runtime-agnostic**: Built on [Vite environment API](https://vite.dev/guide/api-environment.html) and works with other runtimes (e.g., [`@cloudflare/vite-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare)).
- **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:
```sh
npm create vite@latest -- --template rsc
```
## Examples
**Start here:** [`./examples/starter`](./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`](./examples/basic) - Comprehensive showcase of standard RSC features and the primary E2E test fixture.
- [`./examples/use-cache`](./examples/use-cache) - Local cached functions and components demonstrating argument keys, Flight replay, dynamic children, captured values, and invalidation.
- [`./examples/use-cache-callable`](./examples/use-cache-callable) - Cached server references demonstrating inline and file directives, client invocation, argument admission, and encrypted closure captures.
- [`./examples/use-cache-persistent`](./examples/use-cache-persistent) - Persistent callable cache entries demonstrating Flight value replay and dependency-aware development invalidation.
- [`./examples/custom-server-function`](./examples/custom-server-function) - Third-party Server Function directive integration using server reference claims.
- [`./examples/ssg`](./examples/ssg) - Static site generation with MDX and client components for interactivity.
- [`./examples/ppr`](./examples/ppr) - Partial prerendering with a reusable static HTML shell and request-time RSC content.
- [`./examples/no-ssr`](./examples/no-ssr) - RSC application without an SSR environment.
- [`./examples/node-stream`](./examples/node-stream) - Starter application served end to end through Node.js request, response, and stream APIs.
- [`./examples/client-first`](./examples/client-first) - Experimental client-owned page that consumes RSC function results.
- [`./examples/action-reachability`](./examples/action-reachability) - Route-aware dispatch for a retained server action.
- [`./examples/browser-mode`](./examples/browser-mode) - Advanced setup that runs both RSC and React client environments in the browser with custom module loading.
- [`./examples/performance-track`](./examples/performance-track) - Minimal React Server Components performance track probe.
- [`./examples/react-router`](./examples/react-router) - React Router RSC integration
- Demonstrates how to integrate [experimental React Router RSC API](https://remix.run/blog/rsc-preview). React Router now provides [official RSC support](https://reactrouter.com/how-to/react-server-components), so it's recommended to follow React Router's official documentation for the latest integration.
## Basic Concepts
This example is a simplified version of [`./examples/starter`](./examples/starter). You can read [`./examples/starter/src/framework/entry.{rsc,ssr,browser}.tsx`](./examples/starter/src/framework) 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.
```mermaid
graph TD
subgraph "rsc environment"
A["React virtual dom tree"] --> |"[@vitejs/plugin-rsc/rsc/server]
renderToReadableStream"| B1["RSC Stream"];
end
B1 --> B2
B1 --> B3
subgraph "ssr environment"
B2["RSC Stream"] --> |"[@vitejs/plugin-rsc/ssr]
createFromReadableStream"| C1["React virtual dom tree"];
C1 --> |"[react-dom/server]
SSR"| E["HTML String/Stream"];
end
subgraph "client environment"
B3["RSC Stream"] --> |"[@vitejs/plugin-rsc/browser]
createFromReadableStream"| C2["React virtual dom tree"];
C2 --> |"[react-dom/client]
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
```
- [`vite.config.ts`](./examples/starter/vite.config.ts)
```js
import rsc from '@vitejs/plugin-rsc'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
// add plugin
rsc(),
],
// specify entry point for each environment.
environments: {
// `rsc` environment loads modules with `react-server` condition.
// this environment is responsible for:
// - RSC stream serialization (React VDOM -> RSC stream)
// - server functions handling
rsc: {
build: {
rollupOptions: {
input: {
index: './src/framework/entry.rsc.tsx',
},
},
},
},
// `ssr` environment loads modules without `react-server` condition.
// this environment is responsible for:
// - RSC stream deserialization (RSC stream -> React VDOM)
// - traditional SSR (React VDOM -> HTML string/stream)
ssr: {
build: {
rollupOptions: {
input: {
index: './src/framework/entry.ssr.tsx',
},
},
},
},
// client environment is used for hydration and client-side rendering
// this environment is responsible for:
// - RSC stream deserialization (RSC stream -> React VDOM)
// - traditional CSR (React VDOM -> Browser DOM tree mount/hydration)
// - refetch and re-render RSC
// - calling server functions
client: {
build: {
rollupOptions: {
input: {
index: './src/framework/entry.browser.tsx',
},
},
},
},
},
})
```
- [`entry.rsc.tsx`](./examples/starter/src/framework/entry.rsc.tsx)
```tsx
import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc/server'
// the plugin assumes `rsc` entry having default export of request handler
export default async function handler(request: Request): Promise {
// serialize React VDOM to RSC stream
const root = (
Test
)
const rscStream = renderToReadableStream(root)
// respond direct RSC stream request based on framework's convention
if (request.url.endsWith('.rsc')) {
return new Response(rscStream, {
headers: {
'Content-type': 'text/x-component;charset=utf-8',
},
})
}
// delegate to SSR environment for html rendering
// `loadModule` is a helper API provided by the plugin for multi environment interaction.
const ssrEntry = await import.meta.viteRsc.loadModule<
typeof import('./entry.ssr.tsx')
>('ssr', 'index')
const htmlStream = await ssrEntry.handleSsr(rscStream)
// respond html
return new Response(htmlStream, {
headers: {
'Content-type': 'text/html',
},
})
}
// add `import.meta.hot.accept` to handle server module change efficiently
if (import.meta.hot) {
import.meta.hot.accept()
}
```
- [`entry.ssr.tsx`](./examples/starter/src/framework/entry.ssr.tsx)
```tsx
import {
createFromReadableStream,
getClientEntryUrl,
} from '@vitejs/plugin-rsc/ssr'
import { renderToReadableStream } from 'react-dom/server.edge'
export async function handleSsr(rscStream: ReadableStream) {
// deserialize RSC stream back to React VDOM
const root = await createFromReadableStream(rscStream)
// render html (traditional SSR)
const htmlStream = renderToReadableStream(root, {
bootstrapModules: [getClientEntryUrl()],
})
return htmlStream
}
```
- [`entry.browser.tsx`](./examples/starter/src/framework/entry.browser.tsx)
```tsx
import { createFromReadableStream } from '@vitejs/plugin-rsc/browser'
import { hydrateRoot } from 'react-dom/client'
async function main() {
// fetch and deserialize RSC stream back to React VDOM
const rscResponse = await fetch(window.location.href + '.rsc')
const root = await createFromReadableStream(rscResponse.body)
// hydration (traditional CSR)
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`
This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa. When `entryName` is omitted, the function automatically uses the single entry from the target environment's `rollupOptions.input`.
During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process as `RunnableDevEnvironment`. Internally, `loadModule` uses the global `__VITE_ENVIRONMENT_RUNNER_IMPORT__` function to import modules in the target environment (see [`__VITE_ENVIRONMENT_RUNNER_IMPORT__`](#__vite_environment_runner_import__) below).
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. This proxy mechanism uses [turbo-stream](https://github.com/jacob-ebey/turbo-stream) for serializing data types beyond JSON, with custom encoders/decoders to additionally support `Request` and `Response` instances.
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,
```js
// ./entry.rsc.tsx
const ssrModule = await import.meta.viteRsc.loadModule("ssr", "index");
ssrModule.renderHTML(...);
// ./entry.ssr.tsx (with environments.ssr.build.rollupOptions.input.index = "./entry.ssr.tsx")
export function renderHTML(...) {}
```
#### `import.meta.viteRsc.import`
- Type: `(specifier: string, options: { environment: string }) => Promise`
A more ergonomic alternative to `loadModule`:
1. No manual `rollupOptions.input` config needed - entries are auto-discovered
2. Specifier matches the path in `typeof import(...)` type annotations
**Comparison:**
```ts
// Before (loadModule) - requires vite.config.ts:
// environments.ssr.build.rollupOptions.input = { index: './entry.ssr.tsx' }
import.meta.viteRsc.loadModule('ssr', 'index')
// After (import) - no config needed, auto-discovered
import.meta.viteRsc.import(
'./entry.ssr.tsx',
{ environment: 'ssr' },
)
```
During development, this works the same as `loadModule`, using the `__VITE_ENVIRONMENT_RUNNER_IMPORT__` function to import modules in the target environment.
During production build, the plugin auto-discovers these imports and emits them as entries in the target environment. A manifest file (`__vite_rsc_env_imports_manifest.js`) is generated to map module specifiers to their output filenames.
### Available on `rsc` environment
#### `import.meta.viteRsc.loadCss`
> [!NOTE]
> The plugin automatically injects CSS for server components. See the [CSS Support](#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.
```tsx
import './test.css'
import dep from './dep.tsx'
export function ServerPage() {
// this will include css assets for "test.css"
// and any css transitively imported through "dep.tsx"
return (
<>
{import.meta.viteRsc.loadCss()}
...
>
)
}
```
When specifying `loadCss()`, it will collect css through the server module resolved by ``.
```tsx
// virtual:my-framework-helper
export function Assets() {
return <>
{import.meta.viteRsc.loadCss("/routes/home.tsx")}
{import.meta.viteRsc.loadCss("/routes/about.tsx")}
{...}
>
}
// user-app.tsx
import { Assets } from "virtual:my-framework-helper";
export function UserApp() {
return
...
}
```
### Available on `ssr` environment
#### `getClientEntryUrl()`
This returns the URL used to load the client entry specified by `environments.client.build.rollupOptions.input.index`. This can be used with React DOM SSR APIs such as [`renderToReadableStream`](https://react.dev/reference/react-dom/server/renderToReadableStream).
```js
import { getClientEntryUrl } from '@vitejs/plugin-rsc/ssr'
import { renderToReadableStream } from 'react-dom/server.edge'
const htmlStream = await renderToReadableStream(reactNode, {
bootstrapModules: [getClientEntryUrl()],
})
```
`getClientEntryUrl()` cannot be used when the `customClientEntry` plugin option is enabled.
### 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.
```js
import { createFromFetch } from '@vitejs/plugin-rsc/browser'
import.meta.hot.on('rsc:update', async () => {
// re-fetch RSC stream
const rscPayload = await createFromFetch(fetch(window.location.href + '.rsc'))
// re-render ...
})
```
### Global API
#### `__VITE_ENVIRONMENT_RUNNER_IMPORT__`
- Type: `(environmentName: string, id: string) => Promise`
This global function provides a standardized way to import a module in a given environment during development. It is used internally by `import.meta.viteRsc.loadModule` to execute modules in the target environment.
By default, the plugin sets this global to import via the environment's module runner:
```js
globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async (environmentName, id) => {
return server.environments[environmentName].runner.import(id)
}
```
**Custom Environment Integration:**
Frameworks with custom environment setups (e.g., environments running in separate workers or with custom module loading) can override this global to provide their own module import logic.
```js
// Custom logic to import module between multiple environments inside worker
globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async (environmentName, id) => {
return myWorkerRunners[environmentname].import(id)
}
```
This allows `import.meta.viteRsc.loadModule` to work seamlessly with different runtime configurations without requiring changes to user code.
## Plugin API
### `@vitejs/plugin-rsc`
- Type: `rsc: (options?: RscPluginOptions) => Plugin[]`;
```js
import rsc from '@vitejs/plugin-rsc'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
rsc({
// this is only a shorthand of specifying each rollup input via
// `environments[name].build.rollupOptions.input.index`
entries: {
rsc: '...',
ssr: '...',
client: '...',
},
// by default, the plugin sets up middleware
// using `default` export of `rsc` environment `index` entry.
// this behavior can be customized by `serverHandler` option.
serverHandler: false,
// the plugin provides build-time validation of 'server-only' and 'client-only' imports.
// this is enabled by default. See the "server-only and client-only import" section below for details.
validateImports: true,
// by default, the plugin generates a random encryption key for "use server" closure
// arguments at build time and writes it into the build output.
// `defineEncryptionKey` is a JavaScript expression evaluated at runtime instead,
// so the key can be loaded as a runtime secret, rotated without rebuilding,
// and shared by server instances built separately from the same source.
// It isn't meant to keep encrypted closures valid across different app versions.
// cf. https://nextjs.org/docs/app/guides/data-security#overwriting-encryption-keys-advanced
defineEncryptionKey: 'process.env.MY_ENCRYPTION_KEY',
// when `loadModuleDevProxy: true`, `import.meta.viteRsc.loadModule` is implemented
// through `fetch` based RPC, which allows, for example, rsc environment inside
// cloudflare workers to communicate with node ssr environment on main Vite process.
loadModuleDevProxy: true,
// by default, `loadCss()` helper is injected based on certain heuristics.
// if it breaks, it can be opt-out or selectively applied based on files.
rscCssTransform: { filter: (id) => id.includes('/my-app/') },
// see `RscPluginOptions` for full options ...
}),
],
// the same options can be also specified via top-level `rsc` property.
// this allows other plugin to set options via `config` hook.
rsc: {
// ...
},
})
```
## RSC runtime (react-server-dom) API
### `@vitejs/plugin-rsc/rsc/server`
This module provides Vite-integrated RSC runtime APIs based on
`react-server-dom/server.edge` for use in the RSC environment:
- `renderToReadableStream`: RSC serialization (React VDOM -> RSC stream)
- `decodeAction/decodeReply/decodeFormState/loadServerAction`
- `registerClientReference/registerServerReference`
- `createTemporaryReferenceSet`
### `@vitejs/plugin-rsc/rsc/static`
This module provides a Vite-integrated RSC runtime API based on
`react-server-dom/static.edge` for use in the RSC environment:
- `prerender`: static RSC serialization (React VDOM -> RSC stream)
`prerender` supports the same Vite-specific `onClientReference` extension as
`renderToReadableStream`.
### `@vitejs/plugin-rsc/rsc/client`
This module provides Vite-integrated RSC runtime APIs based on
`react-server-dom/client.edge` for use in the RSC environment:
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
- `encodeReply`
- `createClientTemporaryReferenceSet`
Together, these APIs allow a React VDOM to be serialized, saved as an RSC stream, and later deserialized within the same `rsc` environment.
### `@vitejs/plugin-rsc/rsc`
This module re-exports `@vitejs/plugin-rsc/rsc/server` and
`@vitejs/plugin-rsc/rsc/client`.
Prefer the explicit `/rsc/server` and `/rsc/client` entry points when only one protocol side is needed. Besides making API ownership explicit, this allows Vite to avoid loading and bundling the unused `react-server-dom` runtime. Use `/rsc` when both sides are intentionally needed.
#### Vite-specific extension: `renderToReadableStream` (experimental)
> [!NOTE]
> This is a Vite-specific extension to the standard React RSC API. The official `react-server-dom` does not provide this callback mechanism.
`renderToReadableStream` API is extended with an optional third parameter with `onClientReference` callback.
This is invoked whenever a client reference is used in RSC stream rendering.
```ts
function renderToReadableStream(
data: T,
// standard options (e.g. temporaryReferences, onError, etc.)
options?: object,
// vite-specific options
extraOptions?: {
onClientReference?: (metadata: {
id: string
name: string
deps: { js: string[]; css: string[] }
}) => void
},
): ReadableStream
```
#### Vite-specific extension: `decodeReply`, `decodeAction`, and `decodeFormState` (experimental)
> [!NOTE]
> This is a Vite-specific extension to the standard React RSC API. The official `react-server-dom` does not provide this callback mechanism.
`decodeReply`, `decodeAction`, and `decodeFormState` (and `decodeReplyFromBusboy` in `@vitejs/plugin-rsc/rsc/server.node`) are extended with an optional last parameter with `onServerReference` callback.
This is invoked whenever React resolves a server reference while decoding: the action itself in `decodeAction`, and any server function passed as an argument or bound to an action.
`id` and `name` match the `${id}#${name}` id that `loadServerAction` receives.
Throwing from the callback rejects the reference before its module is loaded, which allows a framework to limit decoding to the server functions it exposes for the request.
`loadServerAction` doesn't invoke the callback, so a framework should check the action id it passes there separately.
```ts
function decodeReply(
body: string | FormData,
// standard options (e.g. temporaryReferences)
options?: object,
// vite-specific options
extraOptions?: {
onServerReference?: (metadata: { id: string; name: string }) => void
},
): Promise
// `decodeAction(body, extraOptions?)` and
// `decodeFormState(actionResult, body, extraOptions?)` accept the same `extraOptions`.
```
### `@vitejs/plugin-rsc/ssr`
This module provides Vite-integrated RSC runtime APIs based on
`react-server-dom/client.edge` for use in the SSR environment:
- `createFromReadableStream`: Deserializes an RSC stream into a React VDOM
- `encodeReply`: Serializes server function arguments
- `createTemporaryReferenceSet`: Creates a temporary reference set shared by deserialization and reply serialization
### Node.js stream APIs variants
Node.js environments can use runtime-specific entry points that integrate React's native Node.js stream APIs with Vite's generated manifests:
| Environment | Entry point | Node.js stream API |
| ------------ | ------------------------------------ | ------------------------ |
| RSC server | `@vitejs/plugin-rsc/rsc/server.node` | `renderToPipeableStream` |
| RSC client | `@vitejs/plugin-rsc/rsc/client.node` | `createFromNodeStream` |
| RSC static | `@vitejs/plugin-rsc/rsc/static.node` | `prerenderToNodeStream` |
| RSC combined | `@vitejs/plugin-rsc/rsc.node` | Server and client APIs |
| SSR | `@vitejs/plugin-rsc/ssr.node` | `createFromNodeStream` |
See [`examples/node-stream`](./examples/node-stream) for an end-to-end Node.js handler using these APIs.
### `@vitejs/plugin-rsc/browser`
This module provides Vite-integrated RSC runtime APIs based on
`react-server-dom/client.browser` for use in the browser environment:
- `createFromReadableStream`: Deserializes an RSC stream into a React VDOM
- `createFromFetch`: Deserializes an RSC response from a fetch promise
- `encodeReply`: Serializes server function arguments
- `createTemporaryReferenceSet`: Creates a temporary reference set shared by deserialization and reply serialization
- `setServerCallback`: Configures how server functions are called
### Low-level runtime entry points
The runtime APIs are exposed through two layers:
- `@vitejs/plugin-rsc/rsc/server`, `@vitejs/plugin-rsc/rsc/client`, `@vitejs/plugin-rsc/rsc/static`, `@vitejs/plugin-rsc/ssr`, and `@vitejs/plugin-rsc/browser` are the recommended entry points for application and framework runtime code. They initialize the plugin's built-in module loading using the manifests generated by Vite and re-export the corresponding React runtime APIs described above.
- `@vitejs/plugin-rsc/react/rsc`, `@vitejs/plugin-rsc/react/ssr`, and
`@vitejs/plugin-rsc/react/browser` are low-level runtime adapters. They are
used by code generated for `"use client"` and `"use server"`, and by custom
integrations that provide module loading through `setRequireModule` instead
of using the plugin's generated manifests.
The two layers share the same module loader within an environment, which allows
generated code to use `/react/*` after a top-level entry initializes the
built-in loader. Application code should continue using the top-level entries,
which re-export the same runtime APIs. Direct use of `/react/*` is only needed
by custom integrations that install their own loaders, such as the
[`examples/browser-mode`](./examples/browser-mode) example, which runs the RSC
and React client environments in the browser.
## 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()`](#importmetaviterscloadcss) in most cases.
1. **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 = () => {}`)
2. **CSS Import Detection**: For detected components, the plugin checks if the module imports any CSS files (`.css`, `.scss`, `.sass`, etc.)
3. **Automatic Wrapping**: When both conditions are met, the plugin wraps the component with a CSS injection wrapper:
```tsx
// Before transformation
import './styles.css'
export function Page() {
return Hello
}
// After transformation
import './styles.css'
export function Page() {
return (
<>
{import.meta.viteRsc.loadCss()}
Hello
>
)
}
```
### Using different React versions
By default, `@vitejs/plugin-rsc` includes a [vendored version](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/package.json#L64) of `react-server-dom-webpack`. When `react-server-dom-webpack` is installed in your project's dependencies, the plugin will automatically use it instead, allowing you to use any React version you need.
**[Canary](https://react.dev/community/versioning-policy#canary-channel) or [experimental](https://react.dev/community/versioning-policy#experimental-channel) versions:**
```json
{
"dependencies": {
"react": "canary",
"react-dom": "canary",
"react-server-dom-webpack": "canary"
}
}
```
**Specific versions (e.g., for security updates):**
```json
{
"dependencies": {
"react": "19.2.3",
"react-dom": "19.2.3",
"react-server-dom-webpack": "19.2.3"
}
}
```
### React libraries
React libraries need special handling on the server so that their `"use client"` modules work. The plugin keeps them out of server externals (`noExternal`) and out of dependency pre-bundling (`optimizeDeps.exclude`) in the `rsc` and `ssr` environments.
The plugin finds these libraries by crawling your project's `package.json`. A package counts as a React library when it lists `react` in `peerDependencies`. The crawl only looks inside React libraries, so a package that imports React libraries should list `react` in `peerDependencies` too.
The plugin also accepts `react` in `dependencies`, as a fallback for packages that list it there instead of in `peerDependencies`.
### 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:
```sh
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:
```js
// package name is "my-rsc-framework"
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:
```json
{
"compilerOptions": {
"types": ["vite/client", "@vitejs/plugin-rsc/types"]
}
}
```
```ts
import.meta.viteRsc.loadModule
// ^^^^^^^^^^
// (environmentName: string, entryName?: string) => Promise
```
See also [Vite documentation](https://vite.dev/guide/api-hmr.html#intellisense-for-typescript) 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:
- server-utils.js
```tsx
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()
}
```
- client.js
```tsx
'use client'
import { getData } from './server-utils.js' // ❌ 'server-only' cannot be imported in client build
...
```
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:
- client-utils.js
```tsx
import 'client-only'
export function getStorage(key) {
// This uses browser-only APIs
return window.localStorage.getItem(key)
}
```
- server.js
```tsx
import { getStorage } from './client-utils.js' // ❌ 'client-only' cannot be imported in server build
export function ServerComponent() {
const data = getStorage("settings")
...
}
```
Note that while there are official npm packages [`server-only`](https://www.npmjs.com/package/server-only) and [`client-only`](https://www.npmjs.com/package/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.
#### Barrel packages
A server component that imports one component from a package's barrel entry can fail with `'client-only' cannot be imported in server build`, even though the component it uses is fine on the server:
```tsx
import { Button } from 'some-ui-library' // ❌ fails because of `Calendar`, which imports 'client-only'
```
```js
// node_modules/some-ui-library/index.js
export { Button } from './button.js'
export { Calendar } from './calendar.js'
```
The validation runs on the module graph before tree-shaking, so the unused `calendar.js` and its `client-only` import are still part of the graph, even when the package has `"sideEffects": false`. Dev fails in the same way, because Vite evaluates the barrel and every module it re-exports. Disabling `validateImports` doesn't fix dev reliably either, because other client-only code in an unused module, such as a top-level `createContext` call, can still crash the server.
To fix this, import from the package's subpath entry instead, for example `import { Button } from 'some-ui-library/button'`. To do this without changing each import, rewrite barrel imports with [`@rolldown/plugin-transform-imports`](https://github.com/rolldown/plugins/tree/main/packages/transform-imports), which applies in both dev and build and requires Vite 8:
```js
import transformImports from '@rolldown/plugin-transform-imports'
export default defineConfig({
plugins: [
transformImports({
'some-ui-library': {
transform: [
// `Button` -> `some-ui-library/button`
['[A-Z].*', 'some-ui-library/{{kebabCase member}}'],
// other exports, such as hooks, stay on the barrel entry
['*', 'some-ui-library'],
],
skipDefaultConversion: true,
},
}),
rsc(),
react(),
],
})
```
When configuring the plugin:
- Check that the member-to-subpath mapping matches the package's `exports`. A member that matches no pattern is rewritten to the member name itself, so keep the `'*'` fallback.
- Set `skipDefaultConversion: true` when the subpath entries use named exports. Otherwise the plugin rewrites `import { Button }` to a default import.
- The plugin only rewrites code that Vite transforms, which excludes pre-bundled dependencies. Packages with a `react` peer dependency are not pre-bundled in server environments, so imports of a typical UI library are covered, but a pre-bundled dependency that imports the barrel itself isn't rewritten.
If the subpath entry itself still reaches client-only code, rewriting imports doesn't help. Move that usage into a `"use client"` component instead.
## 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:
- [Waku](https://github.com/wakujs/waku)
- [@lazarv/react-server](https://github.com/lazarv/react-server)
- [@jacob-ebey/vite-react-server-dom](https://github.com/jacob-ebey/vite-plugins/tree/main/packages/vite-react-server-dom)
- [React Router RSC](https://remix.run/blog/rsc-preview)
- [Parcel RSC](https://parceljs.org/recipes/rsc)