Skip to content

Commit 4cf5bd0

Browse files
committed
Add unstable support for RSC Framework Mode
1 parent 2d83c21 commit 4cf5bd0

File tree

10 files changed

+276
-61
lines changed

10 files changed

+276
-61
lines changed

.changeset/funny-gifts-melt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": minor
3+
---
4+
5+
Add unstable support for RSC Framework Mode

docs/how-to/react-server-components.md

Lines changed: 254 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ unstable: true
55

66
# React Server Components
77

8-
[MODES: data]
8+
[MODES: framework, data]
99

1010
<br/>
1111
<br/>
@@ -22,34 +22,266 @@ From the docs:
2222
2323
React 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.
2424

25+
If 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.
26+
27+
RSC support is available in both Framework Mode and Data Mode. For more information on the difference between these modes, see the [Picking a Mode][picking-a-mode] guide.
28+
2529
## Quick Start
2630

2731
The quickest way to get started is with one of our templates.
2832

29-
These templates come with React Router RSC APIs already configured with the respective bundler, offering you out of the box features such as:
33+
These templates come with React Router RSC APIs already configured, offering you out of the box features such as:
3034

3135
- Server Component Routes
3236
- Server Side Rendering (SSR)
3337
- Client Components (via [`"use client"`][use-client-docs] directive)
3438
- Server Functions (via [`"use server"`][use-server-docs] directive)
3539

36-
**Parcel Template**
40+
### RSC Framework Mode Template
3741

38-
The [parcel template][parcel-rsc-template] uses the official React `react-server-dom-parcel` plugin.
42+
The [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].
3943

4044
```shellscript
41-
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-parcel
45+
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-framework-mode
4246
```
4347

44-
**Vite Template**
48+
### RSC Data Mode Templates
49+
50+
When using RSC Data Mode, you can choose between the Vite and Parcel templates.
51+
52+
The [Vite RSC Data Mode template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.
53+
54+
```shellscript
55+
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-vite
56+
```
4557

46-
The [vite template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.
58+
The [Parcel RSC Data Mode template][parcel-rsc-template] uses the official React `react-server-dom-parcel` plugin.
4759

4860
```shellscript
49-
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-vite
61+
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-parcel
62+
```
63+
64+
## RSC Framework Mode
65+
66+
Most APIs and features in RSC Framework Mode are the same as non-RSC Framework Mode, so this guide will focus on the differences.
67+
68+
### New React Router RSC Vite Plugin
69+
70+
RSC Framework Mode uses a different Vite plugin than non-RSC Framework Mode, currently exported as `unstable_reactRouterRSC`.
71+
72+
This 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.
73+
74+
```tsx filename=vite.config.ts
75+
import { defineConfig } from "vite";
76+
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
77+
import rsc from "@vitejs/plugin-rsc";
78+
79+
export default defineConfig({
80+
plugins: [reactRouterRSC(), rsc()],
81+
});
82+
```
83+
84+
### Build Output
85+
86+
The RSC Framework Mode server build file (`build/server/index.js`) now exports a `default` request handler function (`(request: Request) => Promise<Response>`) for document/data requests.
87+
88+
If 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].
89+
90+
For example, in Express:
91+
92+
```tsx filename=start.js
93+
import express from "express";
94+
import requestHandler from "./build/server/index.js";
95+
import { createRequestListener } from "@remix-run/node-fetch-server";
96+
97+
const app = express();
98+
99+
app.use(
100+
"/assets",
101+
express.static("build/client/assets", {
102+
immutable: true,
103+
maxAge: "1y",
104+
}),
105+
);
106+
app.use(express.static("build/client"));
107+
app.use(createRequestListener(requestHandler));
108+
app.listen(3000);
50109
```
51110

52-
## Using RSC with React Router
111+
### React Elements From Loaders/Actions
112+
113+
In RSC Framework Mode, loaders and actions can now return React elements along with other data. These elements will only ever be rendered on the server.
114+
115+
```tsx
116+
import type { Route } from "./+types/route";
117+
118+
export async function loader() {
119+
return {
120+
message: "Message from the server!",
121+
element: <p>Element from the server!</p>,
122+
};
123+
}
124+
125+
export default function Route({
126+
loaderData,
127+
}: Route.ComponentProps) {
128+
return (
129+
<>
130+
<h1>{loaderData.message}</h1>
131+
{loaderData.element}
132+
</>
133+
);
134+
}
135+
```
136+
137+
If 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]:
138+
139+
```tsx filename=src/routes/counter/counter.tsx
140+
"use client";
141+
142+
export function Counter() {
143+
const [count, setCount] = useState(0);
144+
return (
145+
<button onClick={() => setCount(count + 1)}>
146+
Count: {count}
147+
</button>
148+
);
149+
}
150+
```
151+
152+
```tsx filename=src/routes/counter/route.tsx
153+
import type { Route } from "./+types/route";
154+
import { Counter } from "./counter";
155+
156+
export async function loader() {
157+
return {
158+
message: "Message from the server!",
159+
element: (
160+
<>
161+
<p>Element from the server!</p>
162+
<Counter />
163+
</>
164+
),
165+
};
166+
}
167+
168+
export default function Route({
169+
loaderData,
170+
}: Route.ComponentProps) {
171+
return (
172+
<>
173+
<h1>{loaderData.message}</h1>
174+
{loaderData.element}
175+
</>
176+
);
177+
}
178+
```
179+
180+
### Server Component Routes
181+
182+
If a route exports a `ServerComponent` instead of the typical `default` component export, this component along with other route components (`ErrorBoundary`, `HydrateFallback`, `Layout`) will be server components rather than the usual client components.
183+
184+
```tsx
185+
import type { Route } from "./+types/route";
186+
import { Outlet } from "react-router";
187+
import { getMessage } from "./message";
188+
189+
export async function loader() {
190+
return {
191+
message: await getMessage(),
192+
};
193+
}
194+
195+
export function ServerComponent({
196+
loaderData,
197+
}: Route.ComponentProps) {
198+
return (
199+
<>
200+
<h1>Server Component Route</h1>
201+
<p>Message from the server: {loaderData.message}</p>
202+
<Outlet />
203+
</>
204+
);
205+
}
206+
```
207+
208+
If 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]:
209+
210+
```tsx filename=src/routes/counter/counter.tsx
211+
"use client";
212+
213+
export function Counter() {
214+
const [count, setCount] = useState(0);
215+
return (
216+
<button onClick={() => setCount(count + 1)}>
217+
Count: {count}
218+
</button>
219+
);
220+
}
221+
```
222+
223+
```tsx filename=src/routes/counter/route.tsx
224+
import { Counter } from "./counter";
225+
226+
export function ServerComponent() {
227+
return (
228+
<>
229+
<h1>Counter</h1>
230+
<Counter />
231+
</>
232+
);
233+
}
234+
```
235+
236+
### `.server`/`.client` Modules
237+
238+
To avoid confusion with React's `"use server"` and `"use client"` directives, support for [`.server` modules][server-modules] and [`.client` modules][client-modules] is no longer built-in.
239+
240+
If you need either of these features, 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:
241+
242+
```tsx filename=vite.config.ts
243+
import { defineConfig } from "vite";
244+
import { denyImports } from "vite-env-only";
245+
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
246+
import rsc from "@vitejs/plugin-rsc";
247+
248+
export default defineConfig({
249+
plugins: [
250+
denyImports({
251+
client: { files: ["**/.server/*", "**/*.server.*"] },
252+
}),
253+
reactRouterRSC(),
254+
rsc(),
255+
],
256+
});
257+
```
258+
259+
### MDX Route Support
260+
261+
MDX routes are supported in RSC Framework Mode when using `@mdx-js/rollup` v3.1.1+.
262+
263+
Note 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].
264+
265+
### Unsupported Config Options
266+
267+
For the initial unstable release, the following options from `react-router.config.ts` are not yet supported in RSC Framework Mode:
268+
269+
- `buildEnd`
270+
- `prerender`
271+
- `presets`
272+
- `routeDiscovery`
273+
- `serverBundles`
274+
- `ssr: false` (SPA Mode)
275+
- `future.unstable_splitRouteModules`
276+
- `future.unstable_subResourceIntegrity`
277+
278+
Custom build entry files are also not yet supported.
279+
280+
## RSC Data Mode
281+
282+
The RSC Framework Mode APIs described above are built on top of lower-level RSC Data Mode APIs.
283+
284+
RSC 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.
53285

54286
### Configuring Routes
55287

@@ -238,7 +470,7 @@ export default function Root() {
238470
}
239471
```
240472

241-
## Configuring RSC with React Router
473+
### Bundler Configuration
242474

243475
React 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].
244476

@@ -303,7 +535,7 @@ Relevant APIs:
303535

304536
### Parcel
305537

306-
See the [Parcel RSC docs][parcel-rsc-doc] for more information. You can also refer to our [Parcel RSC Parcel template][parcel-rsc-template] to see a working version.
538+
See the [Parcel RSC docs][parcel-rsc-doc] for more information. You can also refer to our [Parcel RSC Data Mode template][parcel-rsc-template] to see a working version.
307539

308540
In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
309541

@@ -553,7 +785,7 @@ createFromReadableStream(getRSCStream()).then(
553785

554786
### Vite
555787

556-
See the [Vite RSC docs][vite-rsc-doc] for more information. You can also refer to our [Vite RSC template][vite-rsc-template] to see a working version.
788+
See 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.
557789

558790
In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
559791

@@ -765,6 +997,7 @@ createFromReadableStream<RSCServerPayload>(
765997
});
766998
```
767999

1000+
[picking-a-mode]: ../start/modes
7681001
[react-server-components-doc]: https://react.dev/reference/rsc/server-components
7691002
[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions
7701003
[use-client-docs]: https://react.dev/reference/rsc/use-client
@@ -773,7 +1006,7 @@ createFromReadableStream<RSCServerPayload>(
7731006
[framework-mode]: ../start/modes#framework
7741007
[custom-framework]: ../start/data/custom
7751008
[parcel-rsc-doc]: https://parceljs.org/recipes/rsc/
776-
[vite-rsc-doc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
1009+
[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
7771010
[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest
7781011
[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest
7791012
[rsc-static-router]: ../api/rsc/RSCStaticRouter
@@ -782,5 +1015,11 @@ createFromReadableStream<RSCServerPayload>(
7821015
[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter
7831016
[express]: https://expressjs.com/
7841017
[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server
785-
[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-parcel
786-
[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-vite
1018+
[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode
1019+
[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-parcel
1020+
[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite
1021+
[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
1022+
[hooks]: https://react.dev/reference/react/hooks
1023+
[vite-env-only]: https://github.com/pcattori/vite-env-only
1024+
[server-modules]: ../api/framework-conventions/server-modules
1025+
[client-modules]: ../api/framework-conventions/client-modules

integration/helpers/rsc-vite-framework/vite.config.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { defineConfig } from "vite";
2+
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
23
import rsc from "@vitejs/plugin-rsc";
3-
import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
4-
5-
const { unstable_reactRouterRSC: reactRouterRSC } =
6-
__INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
74

85
export default defineConfig({
96
plugins: [reactRouterRSC(), rsc()],

integration/helpers/vite.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,8 @@ export const viteConfig = {
138138
!isRsc
139139
? "import { reactRouter } from '@react-router/dev/vite';"
140140
: [
141-
"import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from '@react-router/dev/internal';",
141+
"import { unstable_reactRouterRSC as reactRouterRSC } from '@react-router/dev/vite';",
142142
"import rsc from '@vitejs/plugin-rsc';",
143-
"const { unstable_reactRouterRSC: reactRouter } = __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;",
144143
].join("\n")
145144
}
146145
${args.mdx ? 'import mdx from "@mdx-js/rollup";' : ""}
@@ -156,7 +155,7 @@ export const viteConfig = {
156155
plugins: [
157156
${args.mdx ? "mdx()," : ""}
158157
${args.vanillaExtract ? "vanillaExtractPlugin({ emitCssInSsr: true })," : ""}
159-
reactRouter(),
158+
${isRsc ? "reactRouterRSC()," : "reactRouter(),"}
160159
${isRsc ? "rsc()," : ""}
161160
envOnlyMacros(),
162161
tsconfigPaths()

0 commit comments

Comments
 (0)