UNPKG

32.1 kBMarkdownView Raw
1---
2title: React Server Components
3unstable: true
4---
5
6# React Server Components
7
8[MODES: framework, data]
9
10<br/>
11<br/>
12
13<docs-warning>React Server Components support is experimental and subject to breaking changes in
14minor/patch releases. Please use with caution and pay **very** close attention
15to release notes for relevant changes.</docs-warning>
16
17React Server Components (RSC) refers generally to an architecture and set of APIs provided by React since version 19.
18
19From the docs:
20
21> Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server.
22>
23> <cite>- [React "Server Components" docs][react-server-components-doc]</cite>
24
25React Router provides a set of APIs for integrating with RSC-compatible bundlers, allowing you to leverage [Server Components][react-server-components-doc] and [Server Functions][react-server-functions-doc] in your React Router applications.
26
27If you're unfamiliar with these React features, we recommend reading the official [Server Components documentation][react-server-components-doc] before using React Router's RSC APIs.
28
29RSC support is available in both Framework and Data Modes. For more information on the conceptual difference between these, see ["Picking a Mode"][picking-a-mode]. However, note that the APIs and features differ between RSC and non-RSC modes in ways that this guide will cover in more detail.
30
31## Quick Start
32
33The quickest way to get started is with one of our templates.
34
35These templates come with React Router RSC APIs already configured, offering you out of the box features such as:
36
37- Server Side Rendering (SSR)
38- Server Components
39- Client Components (via [`"use client"`][use-client-docs] directive)
40- Server Functions (via [`"use server"`][use-server-docs] directive)
41
42### RSC Framework Mode Template
43
44The [RSC Framework Mode template][framework-rsc-template] uses the unstable React Router RSC Vite plugin along with the experimental [`@vitejs/plugin-rsc` plugin][vite-plugin-rsc].
45
46```shellscript
47npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-framework-mode
48```
49
50### RSC Data Mode Templates
51
52The [Vite RSC Data Mode template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.
53
54```shellscript
55npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-vite
56```
57
58## RSC Framework Mode
59
60Most APIs and features in RSC Framework Mode are the same as non-RSC Framework Mode, so this guide will focus on the differences.
61
62### New React Router RSC Vite Plugin
63
64RSC Framework Mode uses a different Vite plugin than non-RSC Framework Mode, currently exported as `unstable_reactRouterRSC`.
65
66This new Vite plugin also has a peer dependency on the experimental `@vitejs/plugin-rsc` plugin. Note that the `@vitejs/plugin-rsc` plugin should be placed after the React Router RSC plugin in your Vite config.
67
68```tsx filename=vite.config.ts
69import { defineConfig } from "vite";
70import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
71import rsc from "@vitejs/plugin-rsc";
72
73export default defineConfig({
74 plugins: [reactRouterRSC(), rsc()],
75});
76```
77
78### Build Output
79
80The RSC Framework Mode server build file (`build/server/index.js`) exports a `default` request handler function (`(request: Request) => Promise<Response>`) for document/data requests.
81
82If needed, you can convert this into a [standard Node.js request listener][node-request-listener] for use with Node's built-in `http.createServer` function (or anything that supports it, e.g. [Express][express]) by using the `createRequestListener` function from [@remix-run/node-fetch-server][node-fetch-server].
83
84For example, in Express:
85
86```tsx filename=start.js
87import express from "express";
88import requestHandler from "./build/server/index.js";
89import { createRequestListener } from "@remix-run/node-fetch-server";
90
91const app = express();
92
93app.use(
94 "/assets",
95 express.static("build/client/assets", {
96 immutable: true,
97 maxAge: "1y",
98 }),
99);
100app.use(express.static("build/client"));
101app.use(createRequestListener(requestHandler));
102app.listen(3000);
103```
104
105### React Elements From Loaders/Actions
106
107In RSC Framework Mode, loaders and actions can return React elements along with other data. These elements will only ever be rendered on the server.
108
109```tsx
110import type { Route } from "./+types/route";
111
112export async function loader() {
113 return {
114 message: "Message from the server!",
115 element: <p>Element from the server!</p>,
116 };
117}
118
119export default function Route({
120 loaderData,
121}: Route.ComponentProps) {
122 return (
123 <>
124 <h1>{loaderData.message}</h1>
125 {loaderData.element}
126 </>
127 );
128}
129```
130
131If you need to use client-only features (e.g. [Hooks][hooks], event handlers) within React elements returned from loaders/actions, you'll need to extract components using these features into a [client module][use-client-docs]:
132
133```tsx filename=src/routes/counter/counter.tsx
134"use client";
135
136import { useState } from "react";
137
138export function Counter() {
139 const [count, setCount] = useState(0);
140 return (
141 <button onClick={() => setCount(count + 1)}>
142 Count: {count}
143 </button>
144 );
145}
146```
147
148```tsx filename=src/routes/counter/route.tsx
149import type { Route } from "./+types/route";
150import { Counter } from "./counter";
151
152export async function loader() {
153 return {
154 message: "Message from the server!",
155 element: (
156 <>
157 <p>Element from the server!</p>
158 <Counter />
159 </>
160 ),
161 };
162}
163
164export default function Route({
165 loaderData,
166}: Route.ComponentProps) {
167 return (
168 <>
169 <h1>{loaderData.message}</h1>
170 {loaderData.element}
171 </>
172 );
173}
174```
175
176### Route Server Components
177
178If a route exports a `ServerComponent` instead of the typical `default` component export, the route renders on the server instead of the client. A route module cannot export both `default` and `ServerComponent`.
179
180You can still export client-only annotations like `clientLoader` and `clientAction` alongside a `ServerComponent`. The other route module component exports follow the same client/server split: `ErrorBoundary`, `Layout`, and `HydrateFallback` are client components, while `ServerErrorBoundary`, `ServerLayout`, and `ServerHydrateFallback` render on the server.
181
182The following route module components have their own mutually exclusive server component counterparts:
183
184| Server Component Export | Client Component |
185| ----------------------- | ----------------- |
186| `ServerComponent` | `default` |
187| `ServerErrorBoundary` | `ErrorBoundary` |
188| `ServerLayout` | `Layout` |
189| `ServerHydrateFallback` | `HydrateFallback` |
190
191```tsx
192import type { Route } from "./+types/route";
193import { Outlet } from "react-router";
194import { getMessage } from "./message";
195
196export async function loader() {
197 return {
198 message: await getMessage(),
199 };
200}
201
202export function ServerComponent({
203 loaderData,
204}: Route.ServerComponentProps) {
205 return (
206 <>
207 <h1>Server Component Route</h1>
208 <p>Message from the server: {loaderData.message}</p>
209 <Outlet />
210 </>
211 );
212}
213```
214
215If you need to use client-only features (e.g. [Hooks][hooks], event handlers) within a server-first route, you'll need to extract components using these features into a [client module][use-client-docs]:
216
217```tsx filename=src/routes/counter/counter.tsx
218"use client";
219
220import { useState } from "react";
221
222export function Counter() {
223 const [count, setCount] = useState(0);
224 return (
225 <button onClick={() => setCount(count + 1)}>
226 Count: {count}
227 </button>
228 );
229}
230```
231
232```tsx filename=src/routes/counter/route.tsx
233import { Counter } from "./counter";
234
235export function ServerComponent() {
236 return (
237 <>
238 <h1>Counter</h1>
239 <Counter />
240 </>
241 );
242}
243```
244
245### `.server`/`.client` Modules
246
247To avoid confusion with RSC's `"use server"` and `"use client"` directives, support for [`.server` modules][server-modules] and [`.client` modules][client-modules] is no longer built-in when using RSC Framework Mode.
248
249As an alternative solution that doesn't rely on file naming conventions, we recommend using the `"server-only"` and `"client-only"` imports provided by [`@vitejs/plugin-rsc`][vite-plugin-rsc]. For example, to ensure a module is never accidentally included in the client build, simply import from `"server-only"` as a side effect within your server-only module.
250
251```ts filename=app/utils/db.ts
252import "server-only";
253
254// Rest of the module...
255```
256
257Note that while there are official npm packages [`server-only`][server-only-package] and [`client-only`][client-only-package] created by the React team, they don't need to be installed. `@vitejs/plugin-rsc` internally handles these imports and provides build-time validation instead of runtime errors.
258
259If you'd like to quickly migrate existing code that relies on the `.server` and `.client` file naming conventions, we recommend using the [`vite-env-only` plugin][vite-env-only] directly. For example, to ensure `.server` modules aren't accidentally included in the client build:
260
261```tsx filename=vite.config.ts
262import { defineConfig } from "vite";
263import { denyImports } from "vite-env-only";
264import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
265import rsc from "@vitejs/plugin-rsc";
266
267export default defineConfig({
268 plugins: [
269 denyImports({
270 client: { files: ["**/.server/*", "**/*.server.*"] },
271 }),
272 reactRouterRSC(),
273 rsc(),
274 ],
275});
276```
277
278### MDX Route Support
279
280MDX routes are supported in RSC Framework Mode when using `@mdx-js/rollup` v3.1.1+.
281
282Note that any components exported from an MDX route must also be valid in RSC environments, meaning that they cannot use client-only features like [Hooks][hooks]. Any components that need to use these features should be extracted into a [client module][use-client-docs].
283
284### Custom Entry Files
285
286RSC Framework Mode supports custom entry files, allowing you to customize the behavior of the RSC server, SSR server, and client entry points.
287
288The plugin will automatically detect custom entry files in your `app` directory:
289
290- `app/entry.rsc.ts` (or `.tsx`) - Custom RSC server entry
291- `app/entry.ssr.ts` (or `.tsx`) - Custom SSR server entry
292- `app/entry.client.tsx` - Custom client entry
293
294If these files are not found, React Router will use the default entries provided by the framework.
295
296If you want to inspect the generated defaults before overriding them, you can also use `react-router reveal entry.client`, `react-router reveal entry.rsc`, and `react-router reveal entry.ssr`.
297
298#### Basic Override Pattern
299
300You can create a custom entry file that wraps or extends the default behavior. For example, to add custom logging to the RSC entry:
301
302```ts filename=app/entry.rsc.ts
303import defaultEntry from "@react-router/dev/config/default-rsc-entries/entry.rsc";
304import { RouterContextProvider } from "react-router";
305
306export default {
307 fetch(request: Request): Promise<Response> {
308 console.log(
309 "Custom RSC entry handling request:",
310 request.url,
311 );
312
313 const requestContext = new RouterContextProvider();
314
315 return defaultEntry.fetch(request, requestContext);
316 },
317};
318
319if (import.meta.hot) {
320 import.meta.hot.accept();
321}
322```
323
324Similarly, you can customize the SSR entry:
325
326```ts filename=app/entry.ssr.ts
327import { generateHTML as defaultGenerateHTML } from "@react-router/dev/config/default-rsc-entries/entry.ssr";
328
329export function generateHTML(
330 request: Request,
331 serverResponse: Response,
332): Promise<Response> {
333 console.log(
334 "Custom SSR entry generating HTML for:",
335 request.url,
336 );
337
338 return defaultGenerateHTML(request, serverResponse);
339}
340```
341
342And for the client:
343
344```ts filename=app/entry.client.ts
345import "@react-router/dev/config/default-rsc-entries/entry.client";
346```
347
348#### Copying Default Entries
349
350For more advanced customization, you can copy the default entries and modify them as needed. To find the default entries:
351
3521. In your IDE, use "Go to Definition" (or Cmd/Ctrl+Click) on the default entry import:
353
354 ```ts
355 import defaultEntry from "@react-router/dev/config/default-rsc-entries/entry.rsc";
356 ```
357
3582. Copy the default entry code into your custom file
359
3603. Modify it to suit your needs
361
362The default entries are located at:
363
364- [`@react-router/dev/config/default-rsc-entries/entry.rsc`][entry-rsc-source]
365- [`@react-router/dev/config/default-rsc-entries/entry.ssr`][entry-ssr-source]
366- [`@react-router/dev/config/default-rsc-entries/entry.client`][entry-client-source]
367
368You can view the source code on GitHub using the links above, or navigate directly to these files in `node_modules/@react-router/dev/dist/config/default-rsc-entries/`.
369
370<docs-info>
371
372When copying default entries, make sure to maintain the required exports:
373
374- `entry.rsc.ts` must export a default object with a `fetch` method
375- `entry.ssr.ts` must export a `generateHTML` function
376- `entry.client.tsx` should handle client-side hydration
377
378</docs-info>
379
380### Unsupported Config Options
381
382The following options from `react-router.config.ts` are not currently supported in RSC Framework Mode:
383
384- `buildEnd`
385- `presets`
386- `serverBundles`
387- `splitRouteModules`
388
389## RSC Data Mode
390
391The RSC Framework Mode APIs described above are built on top of lower-level RSC Data Mode APIs.
392
393RSC Data Mode is missing some of the features of RSC Framework Mode (e.g. `routes.ts` config and file system routing, HMR and Hot Data Revalidation), but is more flexible and allows you to integrate with your own bundler and server abstractions.
394
395### Configuring Routes
396
397Routes are configured as an argument to [`matchRSCServerRequest`][match-rsc-server-request]. At a minimum, you need a path and component:
398
399```tsx
400function Root() {
401 return <h1>Hello world</h1>;
402}
403
404matchRSCServerRequest({
405 // ...other options
406 routes: [{ path: "/", Component: Root }],
407});
408```
409
410While you can define components inline, we recommend using the `lazy()` option and defining [Route Modules][route-module] for both startup performance and code organization.
411
412<docs-info>
413
414The `lazy` field of the RSC route config expects the same exports as the [Route Module API][route-module], which keeps the route-module shape consistent across [Framework Mode][framework-mode] and RSC Data Mode.
415
416That includes exports like `loader`, `action`, `meta`, `links`, `headers`, `ErrorBoundary`, `HydrateFallback`, and the client annotations.
417
418</docs-info>
419
420```tsx filename=app/routes.ts
421import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
422
423export function routes() {
424 return [
425 {
426 id: "root",
427 path: "",
428 lazy: () => import("./root/route"),
429 children: [
430 {
431 id: "home",
432 index: true,
433 lazy: () => import("./home/route"),
434 },
435 {
436 id: "about",
437 path: "about",
438 lazy: () => import("./about/route"),
439 },
440 ],
441 },
442 ] satisfies RSCRouteConfig;
443}
444```
445
446### Server Component Routes
447
448By default each route's `default` export renders a Server Component
449
450```tsx
451export default function Home() {
452 return (
453 <main>
454 <article>
455 <h1>Welcome to React Router RSC</h1>
456 <p>
457 You won't find me running any JavaScript in the
458 browser!
459 </p>
460 </article>
461 </main>
462 );
463}
464```
465
466A nice feature of Server Components is that you can fetch data directly from your component by making it asynchronous.
467
468```tsx
469export default async function Home() {
470 let user = await getUserData();
471
472 return (
473 <main>
474 <article>
475 <h1>Welcome to React Router RSC</h1>
476 <p>
477 You won't find me running any JavaScript in the
478 browser!
479 </p>
480 <p>
481 Hello, {user ? user.name : "anonymous person"}!
482 </p>
483 </article>
484 </main>
485 );
486}
487```
488
489<docs-info>
490
491Server Components can also be returned from your loaders and actions. In general, if you are using RSC to build your application, loaders are primarily useful for things like setting `status` codes or returning a `redirect`.
492
493Using Server Components in loaders can be helpful for incremental adoption of RSC.
494
495</docs-info>
496
497### Server Functions
498
499[Server Functions][react-server-functions-doc] are a React feature that allow you to call async functions executed on the server. They're defined with the [`"use server"`][use-server-docs] directive.
500
501<docs-warning>
502
503Treat every Server Function as a public endpoint. The client controls both the
504Server Function identifier and request URL, so do not rely on route middleware
505for authentication or authorization. Server Functions must perform their own
506access control and input validation; use a route `action` when access control
507should be middleware-driven.
508
509</docs-warning>
510
511```tsx
512"use server";
513
514import { unstable_getRequest as getRequest } from "react-router";
515import { requireUser } from "./auth.ts";
516
517export async function updateFavorite(formData: FormData) {
518 let user = await requireUser(getRequest());
519 let movieId = Number(formData.get("id"));
520 let intent = formData.get("intent");
521
522 if (
523 !Number.isSafeInteger(movieId) ||
524 movieId <= 0 ||
525 (intent !== "add" && intent !== "remove")
526 ) {
527 throw new Error("Invalid form submission");
528 }
529
530 if (intent === "add") {
531 await addFavorite(user.id, movieId);
532 } else {
533 await removeFavorite(user.id, movieId);
534 }
535}
536```
537
538```tsx
539import { updateFavorite } from "./action.ts";
540export async function AddToFavoritesForm({
541 movieId,
542}: {
543 movieId: number;
544}) {
545 let isFav = await isFavorite(movieId);
546 return (
547 <form action={updateFavorite}>
548 <input type="hidden" name="id" value={movieId} />
549 <input
550 type="hidden"
551 name="intent"
552 value={isFav ? "remove" : "add"}
553 />
554 <AddToFavoritesButton isFav={isFav} />
555 </form>
556 );
557}
558```
559
560Note that after server functions are called, React Router will automatically revalidate the route and update the UI with the new server content. You don't have to mess around with any cache invalidation.
561
562### Client Properties
563
564Routes are defined on the server at runtime, but we can still provide `clientLoader`, `clientAction`, and `shouldRevalidate` through the utilization of client references and `"use client"`.
565
566```tsx filename=src/routes/root/client.tsx
567"use client";
568
569export function clientAction() {}
570
571export function clientLoader() {}
572
573export function shouldRevalidate() {}
574
575export default function ClientRoot() {
576 return <p>Client route</p>;
577}
578```
579
580We can then re-export these from our lazy loaded route module:
581
582```tsx filename=src/routes/root/route.tsx
583export {
584 clientAction,
585 clientLoader,
586 shouldRevalidate,
587} from "./client";
588
589export default function Root() {
590 // ...
591}
592```
593
594This is also the way we would make an entire route a Client Component.
595
596```tsx filename=src/routes/root/route.tsx lines=[1,11]
597import { default as ClientRoot } from "./route.client";
598export {
599 clientAction,
600 clientLoader,
601 shouldRevalidate,
602} from "./client";
603
604export default function Root() {
605 // Adding a Server Component at the root is required by bundlers
606 // if you're using css side-effects imports.
607 return <ClientRoot />;
608}
609```
610
611### Bundler Configuration
612
613React Router provides several APIs that allow you to easily integrate with RSC-compatible bundlers, useful if you are using React Router Data Mode to make your own [custom framework][custom-framework].
614
615The following steps show how to setup a React Router application to use Server Components (RSC) to server-render (SSR) pages and hydrate them for single-page app (SPA) navigations. You don't have to use SSR (or even client-side hydration) if you don't want to. You can also leverage the HTML generation for Static Site Generation (SSG) or Incremental Static Regeneration (ISR) if you prefer. This guide is meant merely to explain how to wire up all the different APIs for a typically RSC-based application.
616
617### Entry points
618
619Besides our [route definitions](#configuring-routes), we will need to configure the following:
620
6211. A server to handle the incoming request, fetch the RSC payload, and convert it into HTML
6222. A React server to generate RSC payloads
6233. A browser handler to hydrate the generated HTML and set the `callServer` function to support post-hydration server actions
624
625The following naming conventions have been chosen for familiarity and simplicity. Feel free to name and configure your entry points as you see fit.
626
627See the relevant bundler documentation below for specific code examples for each of the following entry points.
628
629These examples all use [express][express] and [@remix-run/node-fetch-server][node-fetch-server] for the server and request handling.
630
631**Routes**
632
633See [Configuring Routes](#configuring-routes).
634
635**Server**
636
637<docs-info>
638
639You don't have to use SSR at all. You can choose to use RSC to "prerender" HTML for Static Site Generation (SSG) or something like Incremental Static Regeneration (ISR).
640
641</docs-info>
642
643`entry.ssr.tsx` is the entry point for the server. It is responsible for handling the request, calling the RSC server, and converting the RSC payload into HTML on document requests (server-side rendering).
644
645Relevant APIs:
646
647- [`routeRSCServerRequest`][route-rsc-server-request]
648- [`RSCStaticRouter`][rsc-static-router]
649
650**RSC Server**
651
652<docs-info>
653
654Even though you have a "React Server" and a server responsible for request handling/SSR, you don't actually need to have 2 separate servers. You can simply have 2 separate module graphs within the same server. This is important because React behaves differently when generating RSC payloads vs. when generating HTML to be hydrated on the client.
655
656</docs-info>
657
658`entry.rsc.tsx` is the entry point for the React Server. It is responsible for matching the request to a route and generating RSC payloads.
659
660Relevant APIs:
661
662- [`matchRSCServerRequest`][match-rsc-server-request]
663
664**Browser**
665
666`entry.browser.tsx` is the entry point for the client. It is responsible for hydrating the generated HTML and setting the `callServer` function to support post-hydration server actions.
667
668Relevant APIs:
669
670- [`createCallServer`][create-call-server]
671- [`getRSCStream`][get-rsc-stream]
672- [`RSCHydratedRouter`][rsc-hydrated-router]
673
674### Vite
675
676See the [@vitejs/plugin-rsc docs][vite-plugin-rsc] for more information. You can also refer to our [Vite RSC Data Mode template][vite-rsc-template] to see a working version.
677
678In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
679
680```shellscript
681npm i -D vite @vitejs/plugin-react @vitejs/plugin-rsc
682```
683
684#### `vite.config.ts`
685
686To configure Vite, add the following to your `vite.config.ts`:
687
688```ts filename=vite.config.ts
689import rsc from "@vitejs/plugin-rsc/plugin";
690import react from "@vitejs/plugin-react";
691import { defineConfig } from "vite";
692
693export default defineConfig({
694 plugins: [
695 react(),
696 rsc({
697 entries: {
698 client: "src/entry.browser.tsx",
699 rsc: "src/entry.rsc.tsx",
700 ssr: "src/entry.ssr.tsx",
701 },
702 }),
703 ],
704});
705```
706
707```tsx filename=src/routes/config.ts
708import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
709
710export function routes() {
711 return [
712 {
713 id: "root",
714 path: "",
715 lazy: () => import("./root/route"),
716 children: [
717 {
718 id: "home",
719 index: true,
720 lazy: () => import("./home/route"),
721 },
722 {
723 id: "about",
724 path: "about",
725 lazy: () => import("./about/route"),
726 },
727 ],
728 },
729 ] satisfies RSCRouteConfig;
730}
731```
732
733#### `entry.ssr.tsx`
734
735The following is a simplified example of a Vite SSR Server.
736
737```tsx filename=src/entry.ssr.tsx
738import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
739import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
740import {
741 unstable_routeRSCServerRequest as routeRSCServerRequest,
742 unstable_RSCStaticRouter as RSCStaticRouter,
743} from "react-router";
744
745export async function generateHTML(
746 request: Request,
747 serverResponse: Response,
748): Promise<Response> {
749 return await routeRSCServerRequest({
750 // The incoming request.
751 request,
752 // The React Server response
753 serverResponse,
754 // Provide the React Server touchpoints.
755 createFromReadableStream,
756 // Render the router to HTML.
757 async renderHTML(getPayload, options) {
758 const payload = await getPayload();
759 const formState =
760 payload.type === "render"
761 ? await payload.formState
762 : undefined;
763
764 const bootstrapScriptContent =
765 await import.meta.viteRsc.loadBootstrapScriptContent(
766 "index",
767 );
768
769 return await renderHTMLToReadableStream(
770 <RSCStaticRouter getPayload={getPayload} />,
771 {
772 ...options,
773 bootstrapScriptContent,
774 formState,
775 signal: request.signal,
776 },
777 );
778 },
779 });
780}
781```
782
783#### `entry.rsc.tsx`
784
785The following is a simplified example of a Vite RSC Server.
786
787```tsx filename=src/entry.rsc.tsx
788import {
789 createTemporaryReferenceSet,
790 decodeAction,
791 decodeFormState,
792 decodeReply,
793 loadServerAction,
794 renderToReadableStream,
795} from "@vitejs/plugin-rsc/rsc";
796import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
797
798import { routes } from "./routes/config";
799
800function fetchServer(request: Request) {
801 return matchRSCServerRequest({
802 // Provide the React Server touchpoints.
803 createTemporaryReferenceSet,
804 decodeAction,
805 decodeFormState,
806 decodeReply,
807 loadServerAction,
808 // The incoming request.
809 request,
810 // The app routes.
811 routes: routes(),
812 // Encode the match with the React Server implementation.
813 generateResponse(match, options) {
814 return new Response(
815 renderToReadableStream(match.payload, options),
816 {
817 status: match.statusCode,
818 headers: match.headers,
819 },
820 );
821 },
822 });
823}
824
825export default async function handler(request: Request) {
826 // Import the generateHTML function from the client environment
827 const ssr = await import.meta.viteRsc.loadModule<
828 typeof import("./entry.ssr")
829 >("ssr", "index");
830
831 return ssr.generateHTML(
832 request,
833 await fetchServer(request),
834 );
835}
836```
837
838#### `entry.browser.tsx`
839
840```tsx filename=src/entry.browser.tsx
841import {
842 createFromReadableStream,
843 createTemporaryReferenceSet,
844 encodeReply,
845 setServerCallback,
846} from "@vitejs/plugin-rsc/browser";
847import { startTransition, StrictMode } from "react";
848import { hydrateRoot } from "react-dom/client";
849import {
850 unstable_createCallServer as createCallServer,
851 unstable_getRSCStream as getRSCStream,
852 unstable_RSCHydratedRouter as RSCHydratedRouter,
853 type unstable_RSCPayload as RSCPayload,
854} from "react-router/dom";
855
856// Create and set the callServer function to support post-hydration server actions.
857setServerCallback(
858 createCallServer({
859 createFromReadableStream,
860 createTemporaryReferenceSet,
861 encodeReply,
862 }),
863);
864
865// Get and decode the initial server payload.
866createFromReadableStream<RSCPayload>(getRSCStream()).then(
867 (payload) => {
868 startTransition(async () => {
869 const formState =
870 payload.type === "render"
871 ? await payload.formState
872 : undefined;
873
874 hydrateRoot(
875 document,
876 <StrictMode>
877 <RSCHydratedRouter
878 createFromReadableStream={
879 createFromReadableStream
880 }
881 payload={payload}
882 />
883 </StrictMode>,
884 {
885 formState,
886 },
887 );
888 });
889 },
890);
891```
892
893## Content Security Policy nonces
894
895A [Content Security Policy][csp] can use a per-response nonce to allow the inline scripts required for RSC hydration without allowing arbitrary inline scripts. The nonce is an HTML concern, so configure it in `entry.ssr.tsx`; it does not need to be passed to `matchRSCServerRequest` or included in the RSC payload.
896
897In RSC Framework Mode, first run `react-router reveal entry.ssr` to create a custom SSR entry. In RSC Data Mode, update your existing SSR entry. Generate a fresh nonce for each document response, then pass it to `routeRSCServerRequest`, the `RSCStaticRouter`, and your CSP response header:
898
899```tsx filename=app/entry.ssr.tsx
900export async function generateHTML(
901 request: Request,
902 serverResponse: Response,
903): Promise<Response> {
904 const nonce = crypto.randomUUID();
905
906 const response = await routeRSCServerRequest({
907 request,
908 serverResponse,
909 createFromReadableStream,
910 nonce,
911 async renderHTML(getPayload, options) {
912 const payload = getPayload();
913 const bootstrapScriptContent =
914 await import.meta.viteRsc.loadBootstrapScriptContent(
915 "index",
916 );
917
918 return renderHTMLToReadableStream(
919 <RSCStaticRouter
920 getPayload={getPayload}
921 nonce={options.nonce}
922 />,
923 {
924 ...options,
925 bootstrapScriptContent,
926 formState: await payload.formState,
927 signal: request.signal,
928 },
929 );
930 },
931 });
932
933 response.headers.set(
934 "Content-Security-Policy",
935 `script-src 'self' 'nonce-${nonce}'`,
936 );
937 return response;
938}
939```
940
941The `nonce` option on `routeRSCServerRequest` applies the nonce to the inline scripts that transfer the RSC payload into the HTML document. Spreading its `renderHTML` options into `renderHTMLToReadableStream` applies the same nonce to scripts generated by React. Passing it to `RSCStaticRouter` makes it the default for nonce-aware components such as `<Links>` and `<ScrollRestoration>`.
942
943The default RSC Framework entry does not generate a nonce. Only generate one when your application also sends a matching CSP header. For statically prerendered pages, prefer CSP hashes or external scripts instead of a per-response nonce.
944
945[picking-a-mode]: ../start/modes
946[csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
947[react-server-components-doc]: https://react.dev/reference/rsc/server-components
948[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions
949[use-client-docs]: https://react.dev/reference/rsc/use-client
950[use-server-docs]: https://react.dev/reference/rsc/use-server
951[route-module]: ../start/framework/route-module
952[framework-mode]: ../start/modes#framework
953[custom-framework]: ../start/data/custom
954[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
955[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest
956[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest
957[rsc-static-router]: ../api/rsc/RSCStaticRouter
958[create-call-server]: ../api/rsc/createCallServer
959[get-rsc-stream]: ../api/rsc/getRSCStream
960[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter
961[express]: https://expressjs.com/
962[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server
963[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode
964[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite
965[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
966[hooks]: https://react.dev/reference/react/hooks
967[vite-env-only]: https://github.com/pcattori/vite-env-only
968[server-modules]: ../api/framework-conventions/server-modules
969[client-modules]: ../api/framework-conventions/client-modules
970[server-only-package]: https://www.npmjs.com/package/server-only
971[client-only-package]: https://www.npmjs.com/package/client-only
972[entry-rsc-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx
973[entry-ssr-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.ssr.tsx
974[entry-client-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.client.tsx