Skip to content

Commit ebf2334

Browse files
Add unstable support for RSC Framework Mode (#14336)
* Add unstable support for RSC Framework Mode * Document `server-only` and `client-only` packages * Clarify "Picking a Mode" section
1 parent 370fa47 commit ebf2334

File tree

10 files changed

+288
-61
lines changed

10 files changed

+288
-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: 266 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,276 @@ 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 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.
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);
109+
```
110+
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 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.
239+
240+
As 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.
241+
242+
```ts filename=app/utils/db.ts
243+
import "server-only";
244+
245+
// Rest of the module...
246+
```
247+
248+
Note 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.
249+
250+
If 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:
251+
252+
```tsx filename=vite.config.ts
253+
import { defineConfig } from "vite";
254+
import { denyImports } from "vite-env-only";
255+
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
256+
import rsc from "@vitejs/plugin-rsc";
257+
258+
export default defineConfig({
259+
plugins: [
260+
denyImports({
261+
client: { files: ["**/.server/*", "**/*.server.*"] },
262+
}),
263+
reactRouterRSC(),
264+
rsc(),
265+
],
266+
});
50267
```
51268

52-
## Using RSC with React Router
269+
### MDX Route Support
270+
271+
MDX routes are supported in RSC Framework Mode when using `@mdx-js/rollup` v3.1.1+.
272+
273+
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].
274+
275+
### Unsupported Config Options
276+
277+
For the initial unstable release, the following options from `react-router.config.ts` are not yet supported in RSC Framework Mode:
278+
279+
- `buildEnd`
280+
- `prerender`
281+
- `presets`
282+
- `routeDiscovery`
283+
- `serverBundles`
284+
- `ssr: false` (SPA Mode)
285+
- `future.unstable_splitRouteModules`
286+
- `future.unstable_subResourceIntegrity`
287+
288+
Custom build entry files are also not yet supported.
289+
290+
## RSC Data Mode
291+
292+
The RSC Framework Mode APIs described above are built on top of lower-level RSC Data Mode APIs.
293+
294+
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.
53295

54296
### Configuring Routes
55297

@@ -238,7 +480,7 @@ export default function Root() {
238480
}
239481
```
240482

241-
## Configuring RSC with React Router
483+
### Bundler Configuration
242484

243485
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].
244486

@@ -303,7 +545,7 @@ Relevant APIs:
303545

304546
### Parcel
305547

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.
548+
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.
307549

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

@@ -549,7 +791,7 @@ createFromReadableStream(getRSCStream()).then(
549791

550792
### Vite
551793

552-
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.
794+
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.
553795

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

@@ -757,6 +999,7 @@ createFromReadableStream<RSCServerPayload>(
757999
});
7581000
```
7591001

1002+
[picking-a-mode]: ../start/modes
7601003
[react-server-components-doc]: https://react.dev/reference/rsc/server-components
7611004
[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions
7621005
[use-client-docs]: https://react.dev/reference/rsc/use-client
@@ -765,7 +1008,7 @@ createFromReadableStream<RSCServerPayload>(
7651008
[framework-mode]: ../start/modes#framework
7661009
[custom-framework]: ../start/data/custom
7671010
[parcel-rsc-doc]: https://parceljs.org/recipes/rsc/
768-
[vite-rsc-doc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
1011+
[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
7691012
[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest
7701013
[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest
7711014
[rsc-static-router]: ../api/rsc/RSCStaticRouter
@@ -774,5 +1017,13 @@ createFromReadableStream<RSCServerPayload>(
7741017
[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter
7751018
[express]: https://expressjs.com/
7761019
[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server
777-
[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-parcel
778-
[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-vite
1020+
[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode
1021+
[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-parcel
1022+
[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite
1023+
[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
1024+
[hooks]: https://react.dev/reference/react/hooks
1025+
[vite-env-only]: https://github.com/pcattori/vite-env-only
1026+
[server-modules]: ../api/framework-conventions/server-modules
1027+
[client-modules]: ../api/framework-conventions/client-modules
1028+
[server-only-package]: https://www.npmjs.com/package/server-only
1029+
[client-only-package]: https://www.npmjs.com/package/client-only

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)