Vite 26 ⚡ React Server Components — @vitejs/plugin-rsc integration
Vite’s React Server Components support is not a framework. It is a plugin that uses the Environment API to provide the low-level primitives that a React framework can build on. The @vitejs/plugin-rsc package is that plugin, and its role is to split the build into three environments—rsc, ssr, and client—and to provide the runtime adapters that connect them . The plugin does not give you a router, a data-fetching layer, or a production server. It gives you the primitives that a framework would use to build those things, which is why the documentation recommends starting with the npm create vite@latest -- --template rsc scaffold rather than wiring the plugin from scratch .
The architecture is the Environment API made concrete. The rsc environment loads modules with the react-server condition, serializes the React VDOM into an RSC stream, and handles Server Functions. The ssr environment loads modules without the react-server condition, deserializes the RSC stream back into a React VDOM, and renders the HTML. The client environment handles hydration, client-side navigation, and Server Function calls . The plugin provides the runtime re-exports for each environment—@vitejs/plugin-rsc/rsc, @vitejs/plugin-rsc/ssr, and @vitejs/plugin-rsc/browser—so the framework code does not import react-server-dom directly .
Key point: @vitejs/plugin-rsc splits the build into rsc, ssr, and client environments. The rsc environment serializes the React tree into an RSC stream, the ssr environment deserializes it and renders HTML, and the client environment hydrates and handles Server Function calls. The plugin is a primitive for framework authors, not a complete framework.
Why the plugin exists as a separate package
The three-environment problem. A traditional SSR application has two environments: client and ssr. An RSC application has three, because the RSC serialization happens in a separate environment from the HTML rendering. The rsc environment must load modules with the react-server condition, which excludes client-only code and includes server-only code. The ssr environment must load modules without that condition, because it needs the client components to render the HTML shell. The client environment loads the browser bundle. The plugin configures these three environments and the module loading between them .
The runtime re-export problem. The react-server-dom package has separate entry points for the server, the SSR client, and the browser client. The plugin re-exports these as @vitejs/plugin-rsc/rsc, @vitejs/plugin-rsc/ssr, and @vitejs/plugin-rsc/browser, and it wires them to Vite’s module manifest so that the client references resolve to the correct chunks . A framework that imports react-server-dom/server.edge directly would bypass the manifest and fail to load the client chunks.
The serverHandler option. By default, the plugin sets up middleware that uses the default export of the rsc environment’s index entry as the request handler. A framework that wants to control the HTTP server—Cloudflare Workers, for example—sets serverHandler: false and provides its own fetch handler . The option is the escape hatch for a framework that does not want the plugin’s middleware.
The trade-off. The plugin is a low-level primitive, and the documentation says so explicitly. It does not provide routing, file-based or otherwise. It does not provide a production server. It does not provide the caching model that Next.js provides. The rsc template is a minimal standalone application, and it is the starting point for a custom setup, not a replacement for a framework . The plugin is the foundation, and the framework is the building.
a. The three environments and the entry points
The plugin is configured with the rsc() function, and the entry points are declared in the entries option or in the environments object. The three environments have distinct responsibilities, and the entry points reflect them.
// vite.config.ts — the three-environment configuration
import rsc from '@vitejs/plugin-rsc';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
rsc({
// The entries shorthand maps each environment to its entry file.
entries: {
rsc: './src/entry.rsc.tsx',
ssr: './src/entry.ssr.tsx',
client: './src/entry.client.tsx',
},
// A framework that provides its own HTTP handler sets this to false.
serverHandler: false,
}),
],
});
The rsc entry exports the request handler. It renders the React tree into an RSC stream and returns the stream as the response. The ssr entry exports a handleSsr function that receives the RSC stream, deserializes it, and renders the HTML. The client entry hydrates the HTML with the client components .
// src/entry.rsc.tsx — the RSC environment
import * as ReactServer from '@vitejs/plugin-rsc/rsc';
export default async function handler(request: Request): Promise<Response> {
// Serialize the React VDOM into an RSC stream.
const root = <App />;
const rscStream = ReactServer.renderToReadableStream(root);
// Delegate to the SSR environment for HTML rendering.
const ssrEntry = await import.meta.viteRsc.loadModule('ssr', 'index');
const htmlStream = await ssrEntry.handleSsr(rscStream);
return new Response(htmlStream, {
headers: { 'Content-Type': 'text/html' },
});
}
// src/entry.ssr.tsx — the SSR environment
import * as ReactClient from '@vitejs/plugin-rsc/ssr';
import * as ReactDOMServer from 'react-dom/server.edge';
export async function handleSsr(rscStream: ReadableStream) {
// Deserialize the RSC stream back into a React VDOM.
const root = await ReactClient.createFromReadableStream(rscStream);
// Render the HTML with the browser entry as the bootstrap script.
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index');
return ReactDOMServer.renderToReadableStream(root, { bootstrapScriptContent });
}
// src/entry.client.tsx — the client environment
import * as ReactClient from '@vitejs/plugin-rsc/browser';
import * as ReactDOMClient from 'react-dom/client';
// Fetch and deserialize the RSC stream.
const rscResponse = await fetch(window.location.href + '.rsc');
const root = await ReactClient.createFromReadableStream(rscResponse.body);
// Hydrate the HTML.
ReactDOMClient.hydrateRoot(document, root);
The import.meta.viteRsc.loadModule helper is the cross-environment communication primitive. It loads a module from another environment at runtime. The import.meta.viteRsc.loadBootstrapScriptContent helper provides the raw JavaScript for the browser entry, which is used with the React DOM SSR API .
b. The CSS handling and the automatic wrapping
The plugin handles CSS code-splitting and injection for server components automatically. It detects a server component by looking for a function export with a capital letter name, a default export with a capital name, or a const export assigned to a function with a capital name. For the detected components, it checks whether the module imports any CSS files, and if both conditions are met, it wraps the component with a CSS injection wrapper . This eliminates the need to manually call import.meta.viteRsc.loadCss() in most cases.
// A server component that imports CSS
import './styles.css';
export function Page() {
return <div>Hello</div>;
}
// The plugin transforms it to inject the CSS automatically.
The import.meta.viteRsc.loadCss() helper is still available for the cases where the automatic detection does not apply. It accepts an optional id parameter that collects the CSS from the server module resolved by that id. A framework can use it to collect the CSS for a route’s server components .
// A manual CSS collection for a route
export function Assets() {
return (
<>
{import.meta.viteRsc.loadCss('/routes/home.tsx')}
{import.meta.viteRsc.loadCss('/routes/about.tsx')}
</>
);
}
The automatic wrapping is a convenience, not a requirement. A framework that already handles CSS injection can opt out of the heuristic with the rscCssTransform filter option, which allows the automatic wrapping to be selectively applied based on file paths .
Complete Example Session
// ============================================
// PART 1: THE VITE CONFIGURATION
// ============================================
// vite.config.ts
import rsc from '@vitejs/plugin-rsc';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
rsc({
entries: {
rsc: './src/entry.rsc.tsx',
ssr: './src/entry.ssr.tsx',
client: './src/entry.client.tsx',
},
serverHandler: false,
}),
],
});
// ============================================
// PART 2: THE RSC ENTRY
// ============================================
// src/entry.rsc.tsx
import * as ReactServer from '@vitejs/plugin-rsc/rsc';
export default async function handler(request: Request): Promise<Response> {
const root = <App />;
const rscStream = ReactServer.renderToReadableStream(root);
if (request.url.endsWith('.rsc')) {
return new Response(rscStream, {
headers: { 'Content-Type': 'text/x-component' },
});
}
const ssrEntry = await import.meta.viteRsc.loadModule('ssr', 'index');
const htmlStream = await ssrEntry.handleSsr(rscStream);
return new Response(htmlStream, {
headers: { 'Content-Type': 'text/html' },
});
}
// ============================================
// PART 3: THE SSR ENTRY
// ============================================
// src/entry.ssr.tsx
import * as ReactClient from '@vitejs/plugin-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');
return ReactDOMServer.renderToReadableStream(root, {
bootstrapScriptContent,
});
}
// ============================================
// PART 4: THE CLIENT ENTRY
// ============================================
// src/entry.client.tsx
import * as ReactClient from '@vitejs/plugin-rsc/browser';
import * as ReactDOMClient from 'react-dom/client';
const rscResponse = await fetch(window.location.href + '.rsc');
const root = await ReactClient.createFromReadableStream(rscResponse.body);
ReactDOMClient.hydrateRoot(document, root);
// ============================================
// PART 5: THE SERVER COMPONENT
// ============================================
// src/App.tsx
import './styles.css';
export function App() {
return (
<div>
<h1>Hello from RSC</h1>
<ClientButton />
</div>
);
}
// The plugin detects the capital-letter export and the CSS import.
// It wraps the component with the CSS injection automatically.
// ============================================
// PART 6: THE CLIENT COMPONENT
// ============================================
// src/ClientButton.tsx
'use client';
import { useState } from 'react';
export function ClientButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
// The 'use client' directive marks the client boundary.
// The plugin splits the module into the client environment.
// ============================================
// PART 7: THE SERVER FUNCTION
// ============================================
// src/actions.ts
'use server';
export async function increment() {
// This runs on the server.
return 1;
}
// The plugin handles the Server Function registration and the encryption.
// ============================================
// PART 8: THE FRAMEWORK INTEGRATION
// ============================================
// React Router 8 and TanStack Router use the plugin directly.
// The plugin is the primitive; the framework provides the routing.
// The serverHandler: false option is for the frameworks
// that provide their own HTTP handler.
// ============================================
// PART 9: THE SCAFFOLD
// ============================================
// The minimal standalone application:
// npm create vite@latest -- --template rsc
//
// The template is the starting point for a custom setup.
// It is not a production framework.
// ============================================
// PART 10: THE VERIFICATION
// ============================================
// [ ] The plugin is installed and added to vite.config.ts.
// [ ] The three entries are declared.
// [ ] The rsc entry exports the request handler.
// [ ] The ssr entry exports the handleSsr function.
// [ ] The client entry hydrates the root.
// [ ] The CSS is injected automatically or via loadCss().
// [ ] The serverHandler option is set if the framework provides its own.
The ten parts covered the configuration, the three entries, the server and client components, the Server Function, the framework integration, the scaffold, and the verification. The through-line is that the plugin provides the environments and the runtime adapters, and the framework provides the routing and the server.
Quick Reference
The plugin options
| Option | Purpose | Default |
|---|---|---|
entries | Maps each environment to its entry file | — |
serverHandler | Sets up middleware using the rsc entry | true |
validateImports | Build-time validation of server-only/client-only | true |
defineEncryptionKey | Encryption key for Server Function argument binding | Build-time generated |
The environments
| Environment | Condition | Responsibility |
|---|---|---|
rsc | react-server | RSC serialization, Server Functions |
ssr | (none) | RSC deserialization, HTML rendering |
client | (none) | Hydration, client navigation, Server Function calls |
The runtime entry points
| Entry point | Re-exports |
|---|---|
@vitejs/plugin-rsc/rsc | react-server-dom/server.edge |
@vitejs/plugin-rsc/ssr | react-server-dom/client.edge |
@vitejs/plugin-rsc/browser | react-server-dom/client.browser |
The import.meta.viteRsc helpers
| Helper | Environment | Purpose |
|---|---|---|
loadModule(env, entry) | Any | Load a module from another environment |
loadBootstrapScriptContent(entry) | ssr | Get the browser entry JavaScript |
loadCss(id) | rsc | Collect CSS for a server module |
Best Practices
✅ Do This:
// Use the entries shorthand
entries: { rsc: '...', ssr: '...', client: '...' } // ✅
// Use the runtime re-exports
import * as ReactServer from '@vitejs/plugin-rsc/rsc'; // ✅
// Set serverHandler: false for a custom HTTP server
rsc({ serverHandler: false }) // ✅
// Use loadModule for cross-environment communication
await import.meta.viteRsc.loadModule('ssr', 'index') // ✅
❌ Don’t Do This:
// Import react-server-dom directly
import { renderToReadableStream } from 'react-server-dom/server.edge'; // ❌
// Expect the plugin to provide routing
// The plugin is a primitive; the framework provides the router. // ❌
// Expect the plugin to provide a production server
// The plugin provides the environments and the runtime adapters. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The RSC stream is not deserialized | The ssr entry is not configured | Add the ssr entry |
| The client does not hydrate | The client entry is not configured | Add the client entry |
| The Server Function fails | The encryption key is not set | Set defineEncryptionKey or use the default |
| The CSS is not injected | The component is not detected as a server component | Use loadCss() manually or check the export name |
Real-World Examples
1. The plugin
rsc({ entries: { rsc: '...', ssr: '...', client: '...' } })
2. The RSC entry
const rscStream = ReactServer.renderToReadableStream(root);
3. The SSR entry
const root = await ReactClient.createFromReadableStream(rscStream);
4. The client entry
ReactDOMClient.hydrateRoot(document, root);
5. The cross-environment load
await import.meta.viteRsc.loadModule('ssr', 'index')
6. The bootstrap script
await import.meta.viteRsc.loadBootstrapScriptContent('index')
7. The CSS collection
import.meta.viteRsc.loadCss('/routes/home.tsx')
8. The server handler off
rsc({ serverHandler: false })
9. The scaffold
npm create vite@latest -- --template rsc
10. The validation
rsc({ validateImports: true })
Visual
The Three Environments
┌──────────────────────────────────────────────────────────────┐
│ RSC, SSR, AND CLIENT │
│ │
│ rsc environment │
│ ┌─────────────────────┐ │
│ │ react-server │ Serializes the React VDOM │
│ │ condition │ into an RSC stream │
│ │ │ Handles Server Functions │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ssr environment │
│ ┌─────────────────────┐ │
│ │ (no react-server │ Deserializes the RSC stream │
│ │ condition) │ Renders the HTML │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ client environment │
│ ┌─────────────────────┐ │
│ │ Browser │ Hydrates the HTML │
│ │ │ Handles client navigation │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Request Flow
┌──────────────────────────────────────────────────────────────┐
│ FROM REQUEST TO RESPONSE │
│ │
│ Browser requests / │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ rsc entry │ Serializes the React tree │
│ │ renderToReadableStream│ │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ ssr entry │ Deserializes the stream │
│ │ createFromReadableStream│ │
│ │ renderToReadableStream│ Renders HTML │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ HTML response │ The browser hydrates with the │
│ │ │ client entry │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Plugin’s Role
┌──────────────────────────────────────────────────────────────┐
│ WHAT THE PLUGIN PROVIDES VS WHAT THE FRAMEWORK PROVIDES │
│ │
│ Plugin Framework │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Three environments │ │ Router │ │
│ │ Runtime adapters │ │ Production server │ │
│ │ Module loading │ │ Caching model │ │
│ │ CSS injection │ │ Deployment │ │
│ │ Server Function │ │ Data fetching │ │
│ │ encryption │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ The plugin is the primitive. The framework is the product. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Package | @vitejs/plugin-rsc |
| Purpose | RSC primitives via the Environment API |
| Environments | rsc, ssr, client |
| Runtime re-exports | /rsc, /ssr, /browser |
serverHandler | Default true; false for custom HTTP servers |
| CSS handling | Automatic wrapping for detected server components |
| Scaffold | npm create vite@latest -- --template rsc |
| Framework status | Primitive, not a framework |
Key takeaways:
- The plugin splits the build into three environments. The
rscenvironment serializes the React VDOM into an RSC stream, thessrenvironment deserializes it and renders HTML, and theclientenvironment hydrates the HTML and handles client navigation . The Environment API is the mechanism that makes the three environments possible in a single dev server. - The plugin provides runtime adapters, not a framework. The
@vitejs/plugin-rsc/rsc,/ssr, and/browserentry points re-export thereact-server-domAPIs and wire them to Vite’s module manifest. The framework provides the router, the production server, and the caching model . - The
serverHandleroption is for custom HTTP servers. By default, the plugin sets up middleware that uses therscentry as the request handler. A framework that provides its own HTTP handler—Cloudflare Workers, for example—setsserverHandler: falseand wires the handler itself . - The CSS handling is automatic for detected server components. The plugin detects server components by the capital-letter export name and wraps them with a CSS injection if the module imports CSS. The
loadCss()helper is still available for the cases the automatic detection does not cover . - The plugin is a primitive for framework authors. The documentation recommends starting with the
rsctemplate and reading the plugin documentation. The plugin does not replace Next.js or Waku; it is the foundation that a framework builds on .
Remember: @vitejs/plugin-rsc is the Vite primitive for React Server Components. It splits the build into three environments—rsc for serialization, ssr for HTML rendering, and client for hydration—and provides the runtime adapters that connect them. The plugin is not a framework. It does not provide a router, a production server, or a caching model. It provides the environments, the module loading, the CSS handling, and the Server Function encryption. The framework provides the rest. The rsc template is the starting point, and the plugin documentation is the reference. The plugin is the foundation, and the framework is the building.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!