Skip to content

Commit d439188

Browse files
authored
feat:! new RSC framework mode module API (#14901)
1 parent 111f3a3 commit d439188

File tree

11 files changed

+199
-256
lines changed

11 files changed

+199
-256
lines changed

.changeset/rsc-module-api.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@react-router/dev": patch
3+
"react-router": patch
4+
---
5+
6+
UNSTABLE RSC FRAMEWORK MODE BREAKING CHANGE - Existing route module exports remain unchanged from stable v7 non-RSC mode, but new exports are added for RSC mode. If you want to use RSC features, you will need to update your route modules to export the new annotations.
7+
8+
If you are using RSC framework mode currently, you will need to update your route modules to the new conventions. The following route module components have their own mutually exclusive server component counterparts:
9+
10+
| Server Component Export | Client Component |
11+
| ----------------------- | ----------------- |
12+
| `ServerComponent` | `default` |
13+
| `ServerErrorBoundary` | `ErrorBoundary` |
14+
| `ServerLayout` | `Layout` |
15+
| `ServerHydrateFallback` | `HydrateFallback` |
16+
17+
If you were previously exporting a `ServerComponent`, your `ErrorBoundary`, `Layout`, and `HydrateFallback` were also server components. If you want to keep those as server components, you can rename them and prefix them with `Server`. If you were previously importing the implementations of those components from a client module, you can simply inline them.
18+
19+
Example:
20+
21+
Before
22+
23+
```tsx
24+
import { ErrorBoundary as ClientErrorBoundary } from "./client";
25+
26+
export function ServerComponent() {
27+
// ...
28+
}
29+
30+
export function ErrorBoundary() {
31+
return <ClientErrorBoundary />;
32+
}
33+
34+
export function Layout() {
35+
// ...
36+
}
37+
38+
export function HydrateFallback() {
39+
// ...
40+
}
41+
```
42+
43+
After
44+
45+
```tsx
46+
export function ServerComponent() {
47+
// ...
48+
}
49+
50+
export function ErrorBoundary() {
51+
// previous implementation of ClientErrorBoundary, this is now a client component
52+
}
53+
54+
export function ServerLayout() {
55+
// rename previous Layout export to ServerLayout to make it a server component
56+
}
57+
58+
export function ServerHydrateFallback() {
59+
// rename previous HydrateFallback export to ServerHydrateFallback to make it a server component
60+
}
61+
```

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ The quickest way to get started is with one of our templates.
3434

3535
These templates come with React Router RSC APIs already configured, offering you out of the box features such as:
3636

37-
- Server Component Routes
3837
- Server Side Rendering (SSR)
38+
- Server Components
3939
- Client Components (via [`"use client"`][use-client-docs] directive)
4040
- Server Functions (via [`"use server"`][use-server-docs] directive)
4141

@@ -171,9 +171,18 @@ export default function Route({
171171
}
172172
```
173173

174-
### Server Component Routes
174+
### Route Server Components
175+
176+
If a route exports a `ServerComponent` instead of the typical `default` component export, this will be a server component rather than the usual client component. A default export and `ServerComponent` can not both be exported from the same route module, but you can still export client-only annotations like `clientLoader` and `clientAction` alongside a `ServerComponent`, along with any other component exports such as `ErrorBoundary` or `Layout`.
175177

176-
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.
178+
The following route module components have their own mutually exclusive server component counterparts:
179+
180+
| Server Component Export | Client Component |
181+
| ----------------------- | ----------------- |
182+
| `ServerComponent` | `default` |
183+
| `ServerErrorBoundary` | `ErrorBoundary` |
184+
| `ServerLayout` | `Layout` |
185+
| `ServerHydrateFallback` | `HydrateFallback` |
177186

178187
```tsx
179188
import type { Route } from "./+types/route";
@@ -188,7 +197,7 @@ export async function loader() {
188197

189198
export function ServerComponent({
190199
loaderData,
191-
}: Route.ComponentProps) {
200+
}: Route.ServerComponentProps) {
192201
return (
193202
<>
194203
<h1>Server Component Route</h1>

integration/typegen-test.ts

Lines changed: 5 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ test.describe("typegen", () => {
718718
export function ServerComponent({
719719
loaderData,
720720
actionData
721-
}: Route.ComponentProps) {
721+
}: Route.ServerComponentProps) {
722722
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string }>>
723723
type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>
724724
@@ -731,10 +731,10 @@ test.describe("typegen", () => {
731731
)
732732
}
733733
734-
export function ErrorBoundary({
734+
export function ServerErrorBoundary({
735735
loaderData,
736736
actionData
737-
}: Route.ErrorBoundaryProps) {
737+
}: Route.ServerErrorBoundaryProps) {
738738
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | undefined>>
739739
type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>
740740
@@ -747,10 +747,10 @@ test.describe("typegen", () => {
747747
)
748748
}
749749
750-
export function HydrateFallback({
750+
export function ServerHydrateFallback({
751751
loaderData,
752752
actionData
753-
}: Route.HydrateFallbackProps) {
753+
}: Route.ServerHydrateFallbackProps) {
754754
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | undefined>>
755755
type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>
756756
@@ -766,95 +766,6 @@ test.describe("typegen", () => {
766766
});
767767
await $("pnpm typecheck");
768768
});
769-
770-
test("when RSC Framework Mode plugin is not present", async ({
771-
edit,
772-
$,
773-
}) => {
774-
await edit({
775-
"vite.config.ts": viteConfig({ rsc: false }),
776-
"app/routes.ts": tsx`
777-
import { type RouteConfig, route } from "@react-router/dev/routes";
778-
779-
export default [
780-
route("server-component/:id", "routes/server-component.tsx")
781-
] satisfies RouteConfig;
782-
`,
783-
"app/routes/server-component.tsx": tsx`
784-
import type { Expect, Equal } from "../expect-type"
785-
import type { Route } from "./+types/server-component"
786-
787-
export function loader({ params }: Route.LoaderArgs) {
788-
type Test = Expect<Equal<typeof params, { id: string} >>
789-
return { server: "server" }
790-
}
791-
792-
export function clientLoader() {
793-
return { client: "client" }
794-
}
795-
796-
export function action() {
797-
return { server: "server" }
798-
}
799-
800-
export function clientAction() {
801-
return { client: "client" }
802-
}
803-
804-
// This export is not used in standard Framework Mode. This is just
805-
// to test that the typegen is unaffected by this export outside of
806-
// RSC Framework Mode.
807-
export function ServerComponent({
808-
loaderData,
809-
actionData
810-
}: Route.ComponentProps) {
811-
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string }>>
812-
type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>
813-
814-
return (
815-
<>
816-
<h1>ServerComponent (unused)</h1>
817-
<p>Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}</p>
818-
{actionData && <p>Action data: {"server" in actionData ? actionData.server : actionData.client}</p>}
819-
</>
820-
)
821-
}
822-
823-
export function ErrorBoundary({
824-
loaderData,
825-
actionData
826-
}: Route.ErrorBoundaryProps) {
827-
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string } | undefined>>
828-
type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>
829-
830-
return (
831-
<>
832-
<h1>ErrorBoundary</h1>
833-
{loaderData && <p>Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}</p>}
834-
{actionData && <p>Action data: {"server" in actionData ? actionData.server : actionData.client}</p>}
835-
</>
836-
)
837-
}
838-
839-
export function HydrateFallback({
840-
loaderData,
841-
actionData
842-
}: Route.HydrateFallbackProps) {
843-
type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string } | undefined>>
844-
type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>
845-
846-
return (
847-
<>
848-
<h1>HydrateFallback</h1>
849-
{loaderData && <p>Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}</p>}
850-
{actionData && <p>Action data: {"server" in actionData ? actionData.server : actionData.client}</p>}
851-
</>
852-
)
853-
}
854-
`,
855-
});
856-
await $("pnpm typecheck");
857-
});
858769
});
859770

860771
test.describe("default export", () => {

packages/react-router-dev/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"include": ["**/*.ts", "./rsc-types.d.ts", "package.json"],
3-
"exclude": ["dist", "__tests__", "node_modules", "**/*-test.ts"],
3+
"exclude": ["dist", "__tests__", "node_modules", "**/*-test.ts", "config/default-rsc-entries"],
44
"compilerOptions": {
55
"lib": ["DOM", "DOM.Iterable", "ES2022"],
66
"types": ["vite/client", "@vitejs/plugin-rsc/types"],

packages/react-router-dev/typegen/generate.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ function getRouteAnnotations({
303303
Babel.generate(matchesType).code +
304304
"\n\n" +
305305
ts`
306-
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, ${ctx.rsc}>;
306+
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
307307
308308
export namespace Route {
309309
// links
@@ -340,11 +340,20 @@ function getRouteAnnotations({
340340
// HydrateFallback
341341
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
342342
343+
// ServerHydrateFallback
344+
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
345+
343346
// Component
344347
export type ComponentProps = Annotations["ComponentProps"];
345348
349+
// ServerComponent
350+
export type ServerComponentProps = Annotations["ServerComponentProps"];
351+
346352
// ErrorBoundary
347353
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
354+
355+
// ServerErrorBoundary
356+
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
348357
}
349358
`;
350359
return { filename, content };

0 commit comments

Comments
 (0)