UNPKG

31.4 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```tsx
502"use server";
503
504export async function updateFavorite(formData: FormData) {
505 let movieId = formData.get("id");
506 let intent = formData.get("intent");
507 if (intent === "add") {
508 await addFavorite(Number(movieId));
509 } else {
510 await removeFavorite(Number(movieId));
511 }
512}
513```
514
515```tsx
516import { updateFavorite } from "./action.ts";
517export async function AddToFavoritesForm({
518 movieId,
519}: {
520 movieId: number;
521}) {
522 let isFav = await isFavorite(movieId);
523 return (
524 <form action={updateFavorite}>
525 <input type="hidden" name="id" value={movieId} />
526 <input
527 type="hidden"
528 name="intent"
529 value={isFav ? "remove" : "add"}
530 />
531 <AddToFavoritesButton isFav={isFav} />
532 </form>
533 );
534}
535```
536
537Note 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.
538
539### Client Properties
540
541Routes 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"`.
542
543```tsx filename=src/routes/root/client.tsx
544"use client";
545
546export function clientAction() {}
547
548export function clientLoader() {}
549
550export function shouldRevalidate() {}
551
552export default function ClientRoot() {
553 return <p>Client route</p>;
554}
555```
556
557We can then re-export these from our lazy loaded route module:
558
559```tsx filename=src/routes/root/route.tsx
560export {
561 clientAction,
562 clientLoader,
563 shouldRevalidate,
564} from "./client";
565
566export default function Root() {
567 // ...
568}
569```
570
571This is also the way we would make an entire route a Client Component.
572
573```tsx filename=src/routes/root/route.tsx lines=[1,11]
574import { default as ClientRoot } from "./route.client";
575export {
576 clientAction,
577 clientLoader,
578 shouldRevalidate,
579} from "./client";
580
581export default function Root() {
582 // Adding a Server Component at the root is required by bundlers
583 // if you're using css side-effects imports.
584 return <ClientRoot />;
585}
586```
587
588### Bundler Configuration
589
590React 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].
591
592The 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.
593
594### Entry points
595
596Besides our [route definitions](#configuring-routes), we will need to configure the following:
597
5981. A server to handle the incoming request, fetch the RSC payload, and convert it into HTML
5992. A React server to generate RSC payloads
6003. A browser handler to hydrate the generated HTML and set the `callServer` function to support post-hydration server actions
601
602The following naming conventions have been chosen for familiarity and simplicity. Feel free to name and configure your entry points as you see fit.
603
604See the relevant bundler documentation below for specific code examples for each of the following entry points.
605
606These examples all use [express][express] and [@remix-run/node-fetch-server][node-fetch-server] for the server and request handling.
607
608**Routes**
609
610See [Configuring Routes](#configuring-routes).
611
612**Server**
613
614<docs-info>
615
616You 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).
617
618</docs-info>
619
620`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).
621
622Relevant APIs:
623
624- [`routeRSCServerRequest`][route-rsc-server-request]
625- [`RSCStaticRouter`][rsc-static-router]
626
627**RSC Server**
628
629<docs-info>
630
631Even 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.
632
633</docs-info>
634
635`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.
636
637Relevant APIs:
638
639- [`matchRSCServerRequest`][match-rsc-server-request]
640
641**Browser**
642
643`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.
644
645Relevant APIs:
646
647- [`createCallServer`][create-call-server]
648- [`getRSCStream`][get-rsc-stream]
649- [`RSCHydratedRouter`][rsc-hydrated-router]
650
651### Vite
652
653See 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.
654
655In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
656
657```shellscript
658npm i -D vite @vitejs/plugin-react @vitejs/plugin-rsc
659```
660
661#### `vite.config.ts`
662
663To configure Vite, add the following to your `vite.config.ts`:
664
665```ts filename=vite.config.ts
666import rsc from "@vitejs/plugin-rsc/plugin";
667import react from "@vitejs/plugin-react";
668import { defineConfig } from "vite";
669
670export default defineConfig({
671 plugins: [
672 react(),
673 rsc({
674 entries: {
675 client: "src/entry.browser.tsx",
676 rsc: "src/entry.rsc.tsx",
677 ssr: "src/entry.ssr.tsx",
678 },
679 }),
680 ],
681});
682```
683
684```tsx filename=src/routes/config.ts
685import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
686
687export function routes() {
688 return [
689 {
690 id: "root",
691 path: "",
692 lazy: () => import("./root/route"),
693 children: [
694 {
695 id: "home",
696 index: true,
697 lazy: () => import("./home/route"),
698 },
699 {
700 id: "about",
701 path: "about",
702 lazy: () => import("./about/route"),
703 },
704 ],
705 },
706 ] satisfies RSCRouteConfig;
707}
708```
709
710#### `entry.ssr.tsx`
711
712The following is a simplified example of a Vite SSR Server.
713
714```tsx filename=src/entry.ssr.tsx
715import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
716import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
717import {
718 unstable_routeRSCServerRequest as routeRSCServerRequest,
719 unstable_RSCStaticRouter as RSCStaticRouter,
720} from "react-router";
721
722export async function generateHTML(
723 request: Request,
724 serverResponse: Response,
725): Promise<Response> {
726 return await routeRSCServerRequest({
727 // The incoming request.
728 request,
729 // The React Server response
730 serverResponse,
731 // Provide the React Server touchpoints.
732 createFromReadableStream,
733 // Render the router to HTML.
734 async renderHTML(getPayload, options) {
735 const payload = await getPayload();
736 const formState =
737 payload.type === "render"
738 ? await payload.formState
739 : undefined;
740
741 const bootstrapScriptContent =
742 await import.meta.viteRsc.loadBootstrapScriptContent(
743 "index",
744 );
745
746 return await renderHTMLToReadableStream(
747 <RSCStaticRouter getPayload={getPayload} />,
748 {
749 ...options,
750 bootstrapScriptContent,
751 formState,
752 signal: request.signal,
753 },
754 );
755 },
756 });
757}
758```
759
760#### `entry.rsc.tsx`
761
762The following is a simplified example of a Vite RSC Server.
763
764```tsx filename=src/entry.rsc.tsx
765import {
766 createTemporaryReferenceSet,
767 decodeAction,
768 decodeFormState,
769 decodeReply,
770 loadServerAction,
771 renderToReadableStream,
772} from "@vitejs/plugin-rsc/rsc";
773import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
774
775import { routes } from "./routes/config";
776
777function fetchServer(request: Request) {
778 return matchRSCServerRequest({
779 // Provide the React Server touchpoints.
780 createTemporaryReferenceSet,
781 decodeAction,
782 decodeFormState,
783 decodeReply,
784 loadServerAction,
785 // The incoming request.
786 request,
787 // The app routes.
788 routes: routes(),
789 // Encode the match with the React Server implementation.
790 generateResponse(match, options) {
791 return new Response(
792 renderToReadableStream(match.payload, options),
793 {
794 status: match.statusCode,
795 headers: match.headers,
796 },
797 );
798 },
799 });
800}
801
802export default async function handler(request: Request) {
803 // Import the generateHTML function from the client environment
804 const ssr = await import.meta.viteRsc.loadModule<
805 typeof import("./entry.ssr")
806 >("ssr", "index");
807
808 return ssr.generateHTML(
809 request,
810 await fetchServer(request),
811 );
812}
813```
814
815#### `entry.browser.tsx`
816
817```tsx filename=src/entry.browser.tsx
818import {
819 createFromReadableStream,
820 createTemporaryReferenceSet,
821 encodeReply,
822 setServerCallback,
823} from "@vitejs/plugin-rsc/browser";
824import { startTransition, StrictMode } from "react";
825import { hydrateRoot } from "react-dom/client";
826import {
827 unstable_createCallServer as createCallServer,
828 unstable_getRSCStream as getRSCStream,
829 unstable_RSCHydratedRouter as RSCHydratedRouter,
830 type unstable_RSCPayload as RSCPayload,
831} from "react-router/dom";
832
833// Create and set the callServer function to support post-hydration server actions.
834setServerCallback(
835 createCallServer({
836 createFromReadableStream,
837 createTemporaryReferenceSet,
838 encodeReply,
839 }),
840);
841
842// Get and decode the initial server payload.
843createFromReadableStream<RSCPayload>(getRSCStream()).then(
844 (payload) => {
845 startTransition(async () => {
846 const formState =
847 payload.type === "render"
848 ? await payload.formState
849 : undefined;
850
851 hydrateRoot(
852 document,
853 <StrictMode>
854 <RSCHydratedRouter
855 createFromReadableStream={
856 createFromReadableStream
857 }
858 payload={payload}
859 />
860 </StrictMode>,
861 {
862 formState,
863 },
864 );
865 });
866 },
867);
868```
869
870## Content Security Policy nonces
871
872A [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.
873
874In 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:
875
876```tsx filename=app/entry.ssr.tsx
877export async function generateHTML(
878 request: Request,
879 serverResponse: Response,
880): Promise<Response> {
881 const nonce = crypto.randomUUID();
882
883 const response = await routeRSCServerRequest({
884 request,
885 serverResponse,
886 createFromReadableStream,
887 nonce,
888 async renderHTML(getPayload, options) {
889 const payload = getPayload();
890 const bootstrapScriptContent =
891 await import.meta.viteRsc.loadBootstrapScriptContent(
892 "index",
893 );
894
895 return renderHTMLToReadableStream(
896 <RSCStaticRouter
897 getPayload={getPayload}
898 nonce={options.nonce}
899 />,
900 {
901 ...options,
902 bootstrapScriptContent,
903 formState: await payload.formState,
904 signal: request.signal,
905 },
906 );
907 },
908 });
909
910 response.headers.set(
911 "Content-Security-Policy",
912 `script-src 'self' 'nonce-${nonce}'`,
913 );
914 return response;
915}
916```
917
918The `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>`.
919
920The 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.
921
922[picking-a-mode]: ../start/modes
923[csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
924[react-server-components-doc]: https://react.dev/reference/rsc/server-components
925[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions
926[use-client-docs]: https://react.dev/reference/rsc/use-client
927[use-server-docs]: https://react.dev/reference/rsc/use-server
928[route-module]: ../start/framework/route-module
929[framework-mode]: ../start/modes#framework
930[custom-framework]: ../start/data/custom
931[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
932[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest
933[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest
934[rsc-static-router]: ../api/rsc/RSCStaticRouter
935[create-call-server]: ../api/rsc/createCallServer
936[get-rsc-stream]: ../api/rsc/getRSCStream
937[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter
938[express]: https://expressjs.com/
939[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server
940[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode
941[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite
942[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
943[hooks]: https://react.dev/reference/react/hooks
944[vite-env-only]: https://github.com/pcattori/vite-env-only
945[server-modules]: ../api/framework-conventions/server-modules
946[client-modules]: ../api/framework-conventions/client-modules
947[server-only-package]: https://www.npmjs.com/package/server-only
948[client-only-package]: https://www.npmjs.com/package/client-only
949[entry-rsc-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx
950[entry-ssr-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.ssr.tsx
951[entry-client-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.client.tsx