diff --git a/.changeset/funny-gifts-melt.md b/.changeset/funny-gifts-melt.md
new file mode 100644
index 0000000000..4549ef4455
--- /dev/null
+++ b/.changeset/funny-gifts-melt.md
@@ -0,0 +1,5 @@
+---
+"@react-router/dev": minor
+---
+
+Add unstable support for RSC Framework Mode
diff --git a/docs/how-to/react-server-components.md b/docs/how-to/react-server-components.md
index 66f29006b0..c38fab5c87 100644
--- a/docs/how-to/react-server-components.md
+++ b/docs/how-to/react-server-components.md
@@ -5,7 +5,7 @@ unstable: true
# React Server Components
-[MODES: data]
+[MODES: framework, data]
@@ -22,34 +22,276 @@ From the docs:
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.
+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.
+
+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.
+
## Quick Start
The quickest way to get started is with one of our templates.
-These templates come with React Router RSC APIs already configured with the respective bundler, offering you out of the box features such as:
+These templates come with React Router RSC APIs already configured, offering you out of the box features such as:
- Server Component Routes
- Server Side Rendering (SSR)
- Client Components (via [`"use client"`][use-client-docs] directive)
- Server Functions (via [`"use server"`][use-server-docs] directive)
-**Parcel Template**
+### RSC Framework Mode Template
-The [parcel template][parcel-rsc-template] uses the official React `react-server-dom-parcel` plugin.
+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].
```shellscript
-npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-parcel
+npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-framework-mode
```
-**Vite Template**
+### RSC Data Mode Templates
+
+When using RSC Data Mode, you can choose between the Vite and Parcel templates.
+
+The [Vite RSC Data Mode template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.
+
+```shellscript
+npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-vite
+```
-The [vite template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.
+The [Parcel RSC Data Mode template][parcel-rsc-template] uses the official React `react-server-dom-parcel` plugin.
```shellscript
-npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-vite
+npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-parcel
+```
+
+## RSC Framework Mode
+
+Most APIs and features in RSC Framework Mode are the same as non-RSC Framework Mode, so this guide will focus on the differences.
+
+### New React Router RSC Vite Plugin
+
+RSC Framework Mode uses a different Vite plugin than non-RSC Framework Mode, currently exported as `unstable_reactRouterRSC`.
+
+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.
+
+```tsx filename=vite.config.ts
+import { defineConfig } from "vite";
+import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
+import rsc from "@vitejs/plugin-rsc";
+
+export default defineConfig({
+ plugins: [reactRouterRSC(), rsc()],
+});
+```
+
+### Build Output
+
+The RSC Framework Mode server build file (`build/server/index.js`) now exports a `default` request handler function (`(request: Request) => Promise`) for document/data requests.
+
+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].
+
+For example, in Express:
+
+```tsx filename=start.js
+import express from "express";
+import requestHandler from "./build/server/index.js";
+import { createRequestListener } from "@remix-run/node-fetch-server";
+
+const app = express();
+
+app.use(
+ "/assets",
+ express.static("build/client/assets", {
+ immutable: true,
+ maxAge: "1y",
+ }),
+);
+app.use(express.static("build/client"));
+app.use(createRequestListener(requestHandler));
+app.listen(3000);
+```
+
+### React Elements From Loaders/Actions
+
+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.
+
+```tsx
+import type { Route } from "./+types/route";
+
+export async function loader() {
+ return {
+ message: "Message from the server!",
+ element:
+ {loaderData.element}
+ >
+ );
+}
+```
+
+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]:
+
+```tsx filename=src/routes/counter/counter.tsx
+"use client";
+
+export function Counter() {
+ const [count, setCount] = useState(0);
+ return (
+
+ );
+}
+```
+
+```tsx filename=src/routes/counter/route.tsx
+import type { Route } from "./+types/route";
+import { Counter } from "./counter";
+
+export async function loader() {
+ return {
+ message: "Message from the server!",
+ element: (
+ <>
+
+ {loaderData.element}
+ >
+ );
+}
+```
+
+### Server Component Routes
+
+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.
+
+```tsx
+import type { Route } from "./+types/route";
+import { Outlet } from "react-router";
+import { getMessage } from "./message";
+
+export async function loader() {
+ return {
+ message: await getMessage(),
+ };
+}
+
+export function ServerComponent({
+ loaderData,
+}: Route.ComponentProps) {
+ return (
+ <>
+
Server Component Route
+
Message from the server: {loaderData.message}
+
+ >
+ );
+}
+```
+
+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]:
+
+```tsx filename=src/routes/counter/counter.tsx
+"use client";
+
+export function Counter() {
+ const [count, setCount] = useState(0);
+ return (
+
+ );
+}
+```
+
+```tsx filename=src/routes/counter/route.tsx
+import { Counter } from "./counter";
+
+export function ServerComponent() {
+ return (
+ <>
+
Counter
+
+ >
+ );
+}
+```
+
+### `.server`/`.client` Modules
+
+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.
+
+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.
+
+```ts filename=app/utils/db.ts
+import "server-only";
+
+// Rest of the module...
+```
+
+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.
+
+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:
+
+```tsx filename=vite.config.ts
+import { defineConfig } from "vite";
+import { denyImports } from "vite-env-only";
+import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
+import rsc from "@vitejs/plugin-rsc";
+
+export default defineConfig({
+ plugins: [
+ denyImports({
+ client: { files: ["**/.server/*", "**/*.server.*"] },
+ }),
+ reactRouterRSC(),
+ rsc(),
+ ],
+});
```
-## Using RSC with React Router
+### MDX Route Support
+
+MDX routes are supported in RSC Framework Mode when using `@mdx-js/rollup` v3.1.1+.
+
+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].
+
+### Unsupported Config Options
+
+For the initial unstable release, the following options from `react-router.config.ts` are not yet supported in RSC Framework Mode:
+
+- `buildEnd`
+- `prerender`
+- `presets`
+- `routeDiscovery`
+- `serverBundles`
+- `ssr: false` (SPA Mode)
+- `future.unstable_splitRouteModules`
+- `future.unstable_subResourceIntegrity`
+
+Custom build entry files are also not yet supported.
+
+## RSC Data Mode
+
+The RSC Framework Mode APIs described above are built on top of lower-level RSC Data Mode APIs.
+
+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.
### Configuring Routes
@@ -238,7 +480,7 @@ export default function Root() {
}
```
-## Configuring RSC with React Router
+### Bundler Configuration
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].
@@ -303,7 +545,7 @@ Relevant APIs:
### Parcel
-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.
+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.
In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
@@ -549,7 +791,7 @@ createFromReadableStream(getRSCStream()).then(
### Vite
-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.
+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.
In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
@@ -757,6 +999,7 @@ createFromReadableStream(
});
```
+[picking-a-mode]: ../start/modes
[react-server-components-doc]: https://react.dev/reference/rsc/server-components
[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions
[use-client-docs]: https://react.dev/reference/rsc/use-client
@@ -765,7 +1008,7 @@ createFromReadableStream(
[framework-mode]: ../start/modes#framework
[custom-framework]: ../start/data/custom
[parcel-rsc-doc]: https://parceljs.org/recipes/rsc/
-[vite-rsc-doc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
+[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest
[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest
[rsc-static-router]: ../api/rsc/RSCStaticRouter
@@ -774,5 +1017,13 @@ createFromReadableStream(
[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter
[express]: https://expressjs.com/
[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server
-[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-parcel
-[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-vite
+[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode
+[parcel-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-parcel
+[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite
+[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
+[hooks]: https://react.dev/reference/react/hooks
+[vite-env-only]: https://github.com/pcattori/vite-env-only
+[server-modules]: ../api/framework-conventions/server-modules
+[client-modules]: ../api/framework-conventions/client-modules
+[server-only-package]: https://www.npmjs.com/package/server-only
+[client-only-package]: https://www.npmjs.com/package/client-only
diff --git a/integration/helpers/rsc-vite-framework/vite.config.ts b/integration/helpers/rsc-vite-framework/vite.config.ts
index 43e581a164..38371adfb6 100644
--- a/integration/helpers/rsc-vite-framework/vite.config.ts
+++ b/integration/helpers/rsc-vite-framework/vite.config.ts
@@ -1,9 +1,6 @@
import { defineConfig } from "vite";
+import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
import rsc from "@vitejs/plugin-rsc";
-import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
-
-const { unstable_reactRouterRSC: reactRouterRSC } =
- __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
export default defineConfig({
plugins: [reactRouterRSC(), rsc()],
diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts
index dbde2b4396..c4ab197abd 100644
--- a/integration/helpers/vite.ts
+++ b/integration/helpers/vite.ts
@@ -138,9 +138,8 @@ export const viteConfig = {
!isRsc
? "import { reactRouter } from '@react-router/dev/vite';"
: [
- "import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from '@react-router/dev/internal';",
+ "import { unstable_reactRouterRSC as reactRouterRSC } from '@react-router/dev/vite';",
"import rsc from '@vitejs/plugin-rsc';",
- "const { unstable_reactRouterRSC: reactRouter } = __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;",
].join("\n")
}
${args.mdx ? 'import mdx from "@mdx-js/rollup";' : ""}
@@ -156,7 +155,7 @@ export const viteConfig = {
plugins: [
${args.mdx ? "mdx()," : ""}
${args.vanillaExtract ? "vanillaExtractPlugin({ emitCssInSsr: true })," : ""}
- reactRouter(),
+ ${isRsc ? "reactRouterRSC()," : "reactRouter(),"}
${isRsc ? "rsc()," : ""}
envOnlyMacros(),
tsconfigPaths()
diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts
index 0a7b1d7f72..9bcd7ac928 100644
--- a/integration/typegen-test.ts
+++ b/integration/typegen-test.ts
@@ -23,22 +23,15 @@ function typecheck(cwd: string) {
return spawnSync(nodeBin, [tscBin], { cwd });
}
-const viteConfig = ({ rsc }: { rsc: boolean } = { rsc: false }) => tsx`
- ${
- rsc
- ? tsx`
- import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from '@react-router/dev/internal';
- const { unstable_reactRouterRSC: reactRouter } = __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
- `
- : tsx`
- import { reactRouter } from "@react-router/dev/vite";
- `
- }
-
- export default {
- plugins: [reactRouter()],
- };
-`;
+const viteConfig = ({ rsc }: { rsc: boolean } = { rsc: false }) => {
+ return tsx`
+ import { ${rsc ? "unstable_reactRouterRSC as reactRouter" : "reactRouter"} } from "@react-router/dev/vite";
+
+ export default {
+ plugins: [reactRouter()],
+ };
+ `;
+};
const expectType = tsx`
export type Expect = T
diff --git a/integration/vite-plugin-order-validation-test.ts b/integration/vite-plugin-order-validation-test.ts
index 152a02b253..20083a090a 100644
--- a/integration/vite-plugin-order-validation-test.ts
+++ b/integration/vite-plugin-order-validation-test.ts
@@ -30,13 +30,10 @@ test.describe("Vite plugin order validation", () => {
{
"vite.config.js": dedent`
import { defineConfig } from "vite";
- import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
+ import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
import rsc from "@vitejs/plugin-rsc";
import mdx from "@mdx-js/rollup";
- const { unstable_reactRouterRSC: reactRouterRSC } =
- __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
-
export default defineConfig({
plugins: [
reactRouterRSC(),
@@ -63,13 +60,10 @@ test.describe("Vite plugin order validation", () => {
{
"vite.config.js": dedent`
import { defineConfig } from "vite";
- import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
+ import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
import rsc from "@vitejs/plugin-rsc";
import mdx from "@mdx-js/rollup";
- const { unstable_reactRouterRSC: reactRouterRSC } =
- __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
-
export default defineConfig({
plugins: [
rsc(),
diff --git a/packages/react-router-dev/internal.ts b/packages/react-router-dev/internal.ts
deleted file mode 100644
index 0b04cc0783..0000000000
--- a/packages/react-router-dev/internal.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { reactRouterRSCVitePlugin as unstable_reactRouterRSC } from "./vite/rsc/plugin";
-
-export const __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ =
- {
- unstable_reactRouterRSC,
- };
diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json
index 37703a96eb..e6c95fe05c 100644
--- a/packages/react-router-dev/package.json
+++ b/packages/react-router-dev/package.json
@@ -29,10 +29,6 @@
"types": "./dist/vite/cloudflare.d.ts",
"default": "./dist/vite/cloudflare.js"
},
- "./internal": {
- "types": "./dist/internal.d.ts",
- "default": "./dist/internal.js"
- },
"./package.json": "./package.json"
},
"imports": {
diff --git a/packages/react-router-dev/vite.ts b/packages/react-router-dev/vite.ts
index ad15c60bb5..c7fcf064ff 100644
--- a/packages/react-router-dev/vite.ts
+++ b/packages/react-router-dev/vite.ts
@@ -1 +1,2 @@
export { reactRouterVitePlugin as reactRouter } from "./vite/plugin";
+export { reactRouterRSCVitePlugin as unstable_reactRouterRSC } from "./vite/rsc/plugin";
diff --git a/playground/rsc-vite-framework/vite.config.ts b/playground/rsc-vite-framework/vite.config.ts
index 9f082024ee..e9a3d03ff2 100644
--- a/playground/rsc-vite-framework/vite.config.ts
+++ b/playground/rsc-vite-framework/vite.config.ts
@@ -1,17 +1,14 @@
import { defineConfig } from "vite";
-import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
+import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
import rsc from "@vitejs/plugin-rsc";
import mdx from "@mdx-js/rollup";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
-const { unstable_reactRouterRSC: reactRouterRsc } =
- __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
-
export default defineConfig({
plugins: [
mdx({ remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter] }),
- reactRouterRsc(),
+ reactRouterRSC(),
rsc(),
],
});