React Server Components (RSC) Strategies in React Router: Framework vs Data Mode Explained
React Router provides two distinct RSC strategies—Framework Mode for turnkey Vite integration and Data Mode for custom bundler setups—both built around three standardized entry files (entry.rsc.tsx, entry.ssr.tsx, entry.client.tsx) and unstable core APIs.
React Router’s implementation of React Server Components (RSC) offers developers a choice between convention-based automation and low-level API control. According to the remix-run/react-router source code, these strategies map to the library’s two primary architectural modes, sharing common entry-point contracts while differing in bundler integration depth.
RSC Framework Mode: The Turnkey Vite Integration
RSC Framework Mode provides a high-level, opinionated integration built atop the standard Framework Mode router. It targets developers who want a plug-and-play RSC experience with minimal custom bundler configuration.
Core Components and APIs
The Framework Mode strategy centers on the unstable_reactRouterRSC Vite plugin, located in packages/react-router-dev/vite/rsc/plugin.ts. This plugin registers virtual route modules, injects HMR runtime, and resolves the three entry files automatically.
Key APIs include:
routeRSCServerRequest– Handles full document requests by fetching the RSC payload, rendering HTML viaRSCStaticRouter, and returning aResponse.RSCStaticRouter– Server-side router that receives the RSC payload and renders the component tree to a readable stream.RSCHydratedRouter– Client-side router that rehydrates server-rendered HTML and enables post-hydration server actions.createCallServer– Low-level helper used byentry.client.tsxto wire up thecallServerfunction for mutations.
Vite Configuration
To enable RSC Framework Mode, register the plugin in your Vite configuration:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import rsc from "@vitejs/plugin-rsc";
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
export default defineConfig({
plugins: [
react(),
// RSC plugin must appear before the official @vitejs/plugin-rsc
reactRouterRSC(),
rsc({
entries: {
client: "src/entry.browser.tsx",
rsc: "src/entry.rsc.tsx",
ssr: "src/entry.ssr.tsx",
},
}),
],
});
The entries object tells the plugin where to find the three entry points. If you place files named entry.rsc.tsx, entry.ssr.tsx, or entry.client.tsx in your app/ directory, the plugin automatically detects and uses them instead of the defaults.
Customizing Entry Files
For custom behavior such as logging, authentication, or custom response headers, override the default entry files:
// app/entry.rsc.tsx
import defaultEntry from "@react-router/dev/config/default-rsc-entries/entry.rsc";
export default {
async fetch(request: Request) {
console.log("RSC request →", request.url);
const ctx = new RouterContextProvider(); // optional per-request context
return defaultEntry.fetch(request, ctx);
},
};
The same pattern applies to entry.ssr.tsx and entry.client.tsx. The default implementations reside in packages/react-router-dev/config/default-rsc-entries/.
Server-Component-First Routes
In Framework Mode, routes can export a ServerComponent instead of the standard default component:
// src/routes/dashboard/route.tsx
import { Outlet } from "react-router";
export async function loader() {
return { user: await fetchUser() };
}
// This runs **only** on the server
export function ServerComponent({ loaderData }: { loaderData: { user: User } }) {
return (
<>
<h1>Welcome, {loaderData.user.name}</h1>
<Outlet />
</>
);
}
When a route returns a Server Component, any client-only logic must be moved to a separate "use client" module and re-exported.
RSC Data Mode: The Low-Level API Strategy
RSC Data Mode provides a lower-level API that mirrors the standard Data Mode router. It does not provide file-system routing or hot-module-replacement infrastructure. Instead, you manually configure the three entry points and call the core RSC APIs directly.
Core APIs
All Data Mode APIs are prefixed with unstable_ and located in packages/react-router/lib/rsc/server.rsc.tsx:
unstable_matchRSCServerRequest– Matches a request against a user-provided route config and returns anRSCServerPayload.unstable_routeRSCServerRequest– Handles the full request/response lifecycle, similar torouteRSCServerRequestbut expects manual route configuration.unstable_RSCStaticRouter– Server-side router that renders the payload to HTML.unstable_RSCHydratedRouter– Client-side router for hydration.
Implementation Example
A minimal Data Mode setup requires manual route configuration and entry point wiring:
// src/routes/config.ts
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
export function routes() {
return [
{
id: "root",
path: "",
lazy: () => import("./root/route"),
children: [
{
id: "home",
index: true,
lazy: () => import("./home/route"),
},
],
},
] satisfies RSCRouteConfig;
}
// src/entry.rsc.tsx
import {
unstable_matchRSCServerRequest as matchRSCServerRequest,
unstable_RSCStaticRouter,
} from "react-router";
import { routes } from "./routes/config";
export default async function handler(request: Request) {
return matchRSCServerRequest({
request,
routes: routes(),
// Use @vitejs/plugin-rsc runtime helpers
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
loadServerAction,
generateResponse(match) {
return new Response(renderToReadableStream(match.payload), {
status: match.statusCode,
headers: match.headers,
});
},
});
}
// src/entry.ssr.tsx
import {
unstable_routeRSCServerRequest as routeRSCServerRequest,
unstable_RSCStaticRouter,
} from "react-router";
import { routes } from "./routes/config";
export async function generateHTML(request: Request, serverResponse: Response) {
return routeRSCServerRequest({
request,
serverResponse,
// Same RSC runtime helpers as above
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
loadServerAction,
// Render HTML from the static router
async renderHTML(getPayload) {
const payload = getPayload();
return renderToReadableStream(<RSCStaticRouter getPayload={getPayload} />);
},
});
}
// src/entry.browser.tsx
import {
unstable_createCallServer as createCallServer,
unstable_getRSCStream as getRSCStream,
unstable_RSCHydratedRouter as RSCHydratedRouter,
} from "react-router";
setServerCallback(
createCallServer({
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
})
);
createFromReadableStream(getRSCStream()).then((payload) => {
hydrateRoot(
document,
<RSCHydratedRouter payload={payload} />,
);
});
Key Difference: In Data Mode you import the low-level helpers (createTemporaryReferenceSet, decodeAction, etc.) from @vitejs/plugin-rsc directly, whereas the Framework plugin injects them automatically.
When to Choose Data Mode
Select RSC Data Mode when:
- You need to integrate with a non-Vite bundler (e.g., Webpack, Rollup).
- You want to keep the router configuration separate from the file system (e.g., generating routes from a database).
- You prefer to control the three entry points yourself without the extra virtual module layer.
Shared Concepts Across Both Strategies
Regardless of which RSC strategy you choose, React Router enforces a consistent architecture:
Entry-File Detection
The framework plugin looks for app/entry.rsc.ts(x), entry.ssr.ts(x), and entry.client.ts(x). If missing, default generated files from packages/react-router-dev/config/default-rsc-entries/ are used.
Virtual Route Modules
In Framework Mode, each route’s exports transform into a virtual module (?route-module suffix). This enables HMR for server-only changes and provides a unified export shape. The transformation logic lives in packages/react-router-dev/vite/rsc/virtual-route-modules.ts.
Hot Module Replacement (HMR)
The plugin injects a small runtime (unstable_rsc/runtime) that forwards updates to the client via window.__reactRouterRouteModuleUpdates. See the addRefreshWrapper implementation in packages/react-router-dev/vite/rsc/plugin.ts.
Client-Server Boundaries
Use the "use client" directive or the server-only / client-only imports from @vitejs/plugin-rsc to separate concerns. The documentation explicitly advises against the old .server/.client file-naming convention when using RSC Framework Mode.
Server Functions
Server-only functions annotated with "use server" can be called from client components. The RSC payload includes a callServer implementation set up by createCallServer in entry.client.tsx.
Summary
- Two distinct strategies exist for React Server Components in React Router: Framework Mode for turnkey Vite integration and Data Mode for custom bundler control.
- Both strategies require three specific entry files:
entry.rsc.tsx(payload generation),entry.ssr.tsx(HTML rendering), andentry.client.tsx(hydration). - Framework Mode uses the
unstable_reactRouterRSCVite plugin located inpackages/react-router-dev/vite/rsc/plugin.tsto handle virtual modules and HMR automatically. - Data Mode exposes low-level APIs like
unstable_matchRSCServerRequestandunstable_routeRSCServerRequestfrompackages/react-router/lib/rsc/server.rsc.tsxfor manual configuration. - Both approaches support Server Components via
ServerComponentexports (Framework) or manual payload handling (Data), with client boundaries defined by"use client"directives.
Frequently Asked Questions
What is the difference between RSC Framework Mode and RSC Data Mode in React Router?
RSC Framework Mode provides a high-level, convention-based integration with Vite, featuring automatic virtual route modules, hot module replacement, and file-system routing. RSC Data Mode offers low-level APIs that require manual configuration of routes and entry points, making it suitable for custom bundlers or non-Vite environments. Both use the same three entry files (entry.rsc.tsx, entry.ssr.tsx, entry.client.tsx) but differ in how those files are resolved and executed.
How do I enable React Server Components in a React Router project?
To enable RSCs, install the @react-router/dev package and add the unstable_reactRouterRSC plugin to your vite.config.ts. Create the three required entry files (app/entry.rsc.tsx, app/entry.ssr.tsx, app/entry.client.tsx) or rely on the defaults provided in packages/react-router-dev/config/default-rsc-entries/. For custom behavior, override the default entries by exporting a fetch function in entry.rsc.tsx or the respective handlers in the other entries.
What are the three entry files required for React Router RSC strategies?
React Router RSC requires three specific entry points: entry.rsc.tsx generates the RSC payload for server requests and is located at packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx by default; entry.ssr.tsx renders the payload to HTML for the initial document response; and entry.client.tsx hydrates the HTML in the browser and sets up the callServer function for post-hydration actions. Both RSC strategies (Framework and Data) rely on this same entry-point contract.
When should I use RSC Data Mode instead of Framework Mode?
Choose RSC Data Mode when you need to integrate with a non-Vite bundler like Webpack or Rollup, when you want to generate routes from a database or API rather than the file system, or when you require fine-grained control over the build pipeline without the virtual module layer that Framework Mode injects. Data Mode exposes low-level APIs like unstable_matchRSCServerRequest and unstable_routeRSCServerRequest from packages/react-router/lib/rsc/server.rsc.tsx, requiring you to manually wire up the RSC runtime helpers from @vitejs/plugin-rsc.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →