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:

Element from the server!

, + }; +} + +export default function Route({ + loaderData, +}: Route.ComponentProps) { + return ( + <> +

{loaderData.message}

+ {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: ( + <> +

Element from the server!

+ + + ), + }; +} + +export default function Route({ + loaderData, +}: Route.ComponentProps) { + return ( + <> +

{loaderData.message}

+ {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(), ], });