| | |

Vite 26 ⚡ React Server Components — @vitejs/plugin-rsc integration

⏱️ Estimated read: 13 min | |

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

OptionPurposeDefault
entriesMaps each environment to its entry file—
serverHandlerSets up middleware using the rsc entrytrue
validateImportsBuild-time validation of server-only/client-onlytrue
defineEncryptionKeyEncryption key for Server Function argument bindingBuild-time generated

The environments

EnvironmentConditionResponsibility
rscreact-serverRSC serialization, Server Functions
ssr(none)RSC deserialization, HTML rendering
client(none)Hydration, client navigation, Server Function calls

The runtime entry points

Entry pointRe-exports
@vitejs/plugin-rsc/rscreact-server-dom/server.edge
@vitejs/plugin-rsc/ssrreact-server-dom/client.edge
@vitejs/plugin-rsc/browserreact-server-dom/client.browser

The import.meta.viteRsc helpers

HelperEnvironmentPurpose
loadModule(env, entry)AnyLoad a module from another environment
loadBootstrapScriptContent(entry)ssrGet the browser entry JavaScript
loadCss(id)rscCollect 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

PitfallWhy It HappensFix
The RSC stream is not deserializedThe ssr entry is not configuredAdd the ssr entry
The client does not hydrateThe client entry is not configuredAdd the client entry
The Server Function failsThe encryption key is not setSet defineEncryptionKey or use the default
The CSS is not injectedThe component is not detected as a server componentUse 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

ItemValue
Package@vitejs/plugin-rsc
PurposeRSC primitives via the Environment API
Environmentsrsc, ssr, client
Runtime re-exports/rsc, /ssr, /browser
serverHandlerDefault true; false for custom HTTP servers
CSS handlingAutomatic wrapping for detected server components
Scaffoldnpm create vite@latest -- --template rsc
Framework statusPrimitive, not a framework

Key takeaways:

  • The plugin splits the build into three environments. The rsc environment serializes the React VDOM into an RSC stream, the ssr environment deserializes it and renders HTML, and the client environment 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 /browser entry points re-export the react-server-dom APIs and wire them to Vite’s module manifest. The framework provides the router, the production server, and the caching model .
  • The serverHandler option is for custom HTTP servers. By default, the plugin sets up middleware that uses the rsc entry as the request handler. A framework that provides its own HTTP handler—Cloudflare Workers, for example—sets serverHandler: false and 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 rsc template 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.

Was this helpful?


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!