UNPKG

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