Skip to content

Commit 11661ea

Browse files
committed
chore(rsc): update default RSC entries to support formState for progressive enahnced actions
1 parent 4a42011 commit 11661ea

File tree

11 files changed

+112
-21
lines changed

11 files changed

+112
-21
lines changed

.changeset/wet-turtles-enjoy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
update default RSC entries to support formState for progressive enahnced actions

integration/helpers/rsc-parcel-framework/app/entry.browser.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ setServerCallback(
2525
);
2626

2727
createFromReadableStream(getRSCStream()).then((payload: RSCPayload) => {
28-
React.startTransition(() => {
28+
// @ts-expect-error - on 18 types, requires 19.
29+
startTransition(async () => {
30+
const formState =
31+
payload.type === "render" ? await payload.formState : undefined;
32+
2933
hydrateRoot(
3034
document,
3135
React.createElement(
@@ -36,6 +40,10 @@ createFromReadableStream(getRSCStream()).then((payload: RSCPayload) => {
3640
payload,
3741
}),
3842
),
43+
{
44+
// @ts-expect-error - no types for this yet
45+
formState,
46+
},
3947
);
4048
});
4149
});

integration/helpers/rsc-parcel-framework/app/entry.rsc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {
44
createTemporaryReferenceSet,
55
decodeAction,
6+
decodeFormState,
67
decodeReply,
78
loadServerAction,
89
renderToReadableStream,
@@ -18,6 +19,7 @@ export function fetchServer(request: Request) {
1819
return matchRSCServerRequest({
1920
createTemporaryReferenceSet,
2021
decodeAction,
22+
decodeFormState,
2123
decodeReply,
2224
loadServerAction,
2325
request,

integration/helpers/rsc-parcel-framework/app/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ export const requestHandler = async (request: Request) => {
1616
fetchServer,
1717
createFromReadableStream,
1818
async renderHTML(getPayload) {
19+
const payload = await getPayload();
20+
const formState =
21+
payload.type === "render" ? await payload.formState : undefined;
22+
1923
return await renderToReadableStream(
2024
React.createElement(unstable_RSCStaticRouter, { getPayload }),
2125
{
2226
bootstrapScriptContent: (
2327
fetchServer as unknown as { bootstrapScript: string }
2428
).bootstrapScript,
29+
formState,
2530
},
2631
);
2732
},

integration/helpers/rsc-vite/src/entry.browser.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ setServerCallback(
2323
);
2424

2525
createFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {
26-
startTransition(() => {
26+
// @ts-expect-error - on 18 types, requires 19.
27+
startTransition(async () => {
28+
const formState =
29+
payload.type === "render" ? await payload.formState : undefined;
30+
2731
hydrateRoot(
2832
document,
2933
<StrictMode>
@@ -33,6 +37,10 @@ createFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {
3337
getContext={getContext}
3438
/>
3539
</StrictMode>,
40+
{
41+
// @ts-expect-error - no types for this yet
42+
formState,
43+
},
3644
);
3745
});
3846
});

integration/helpers/rsc-vite/src/entry.rsc.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
createTemporaryReferenceSet,
33
decodeAction,
4+
decodeFormState,
45
decodeReply,
56
loadServerAction,
67
renderToReadableStream,
@@ -16,6 +17,7 @@ export async function fetchServer(request: Request) {
1617
createTemporaryReferenceSet,
1718
decodeReply,
1819
decodeAction,
20+
decodeFormState,
1921
loadServerAction,
2022
request,
2123
requestContext,

integration/helpers/rsc-vite/src/entry.ssr.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ export default async function handler(
1616
request,
1717
fetchServer,
1818
createFromReadableStream,
19-
renderHTML(getPayload) {
19+
async renderHTML(getPayload) {
20+
const payload = await getPayload();
21+
const formState =
22+
payload.type === "render" ? await payload.formState : undefined;
23+
2024
return ReactDomServer.renderToReadableStream(
2125
<RSCStaticRouter getPayload={getPayload} />,
2226
{
2327
bootstrapScriptContent,
2428
signal: request.signal,
29+
formState,
2530
},
2631
);
2732
},

integration/rsc/rsc-nojs-test.ts

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,16 @@ import { implementations, js, setupRscTest, validateRSCHtml } from "./utils";
66
test.use({ javaScriptEnabled: false });
77

88
implementations.forEach((implementation) => {
9-
let stop: () => void;
10-
11-
test.afterEach(() => {
12-
stop?.();
13-
});
14-
159
test.describe(`RSC nojs (${implementation.name})`, () => {
16-
test("Supports React Server Functions side-effect redirect headers for document requests", async ({
17-
page,
18-
}, { project }) => {
19-
test.skip(
20-
implementation.name === "parcel" || project.name !== "chromium",
21-
"TODO: figure out why parcel isn't working here",
22-
);
10+
let port: number;
11+
let stop: () => void;
2312

24-
let port = await getPort();
13+
test.afterAll(() => {
14+
stop?.();
15+
});
16+
17+
test.beforeAll(async () => {
18+
port = await getPort();
2519
stop = await setupRscTest({
2620
implementation,
2721
port,
@@ -34,6 +28,10 @@ implementations.forEach((implementation) => {
3428
redirect("/?redirected=true", { headers: { "x-test": "test" } });
3529
return "redirected";
3630
}
31+
32+
export async function incrementAction(prev) {
33+
return prev + 1;
34+
}
3735
`,
3836
"src/routes/home.client.tsx": js`
3937
"use client";
@@ -47,7 +45,7 @@ implementations.forEach((implementation) => {
4745
"src/routes/home.tsx": js`
4846
"use client";
4947
import {useActionState} from "react";
50-
import { redirectAction } from "./home.actions";
48+
import { redirectAction, incrementAction } from "./home.actions";
5149
import { Counter } from "./home.client";
5250
5351
export default function HomeRoute(props) {
@@ -61,12 +59,34 @@ implementations.forEach((implementation) => {
6159
</form>
6260
{state && <div data-testid="state">{state}</div>}
6361
<Counter />
62+
<TestActionState />
6463
</div>
6564
);
6665
}
66+
67+
function TestActionState() {
68+
const [state, action] = useActionState(incrementAction, 0);
69+
return (
70+
<form action={action}>
71+
<button type="submit" data-action-state-increment-submit>
72+
action-state-increment
73+
</button>
74+
<div data-action-state-increment-result>{state}</div>
75+
</form>
76+
);
77+
}
6778
`,
6879
},
6980
});
81+
});
82+
83+
test("Supports React Server Functions side-effect redirect headers for document requests", async ({
84+
page,
85+
}, { project }) => {
86+
test.skip(
87+
implementation.name === "parcel" || project.name !== "chromium",
88+
"TODO: figure out why parcel isn't working here",
89+
);
7090

7191
await page.goto(`http://localhost:${port}/`);
7292

@@ -89,5 +109,25 @@ implementations.forEach((implementation) => {
89109
// Ensure this is using RSC
90110
validateRSCHtml(await page.content());
91111
});
112+
113+
test("Supports form state without JS", async ({ page }, { project }) => {
114+
test.skip(
115+
implementation.name === "parcel" || project.name !== "chromium",
116+
"TODO: figure out why parcel isn't working here",
117+
);
118+
119+
await page.goto(`http://localhost:${port}/`);
120+
121+
await expect(
122+
page.locator("[data-action-state-increment-result]"),
123+
).toHaveText("0");
124+
await page.click("[data-action-state-increment-submit]");
125+
await expect(
126+
page.locator("[data-action-state-increment-result]"),
127+
).toHaveText("1");
128+
129+
// Ensure this is using RSC
130+
validateRSCHtml(await page.content());
131+
});
92132
});
93133
});

packages/react-router-dev/config/default-rsc-entries/entry.client.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ setServerCallback(
2424
);
2525

2626
createFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {
27-
startTransition(() => {
27+
// @ts-expect-error - on 18 types, requires 19.
28+
startTransition(async () => {
29+
const formState =
30+
payload.type === "render" ? await payload.formState : undefined;
31+
2832
hydrateRoot(
2933
document,
3034
<StrictMode>
@@ -33,6 +37,10 @@ createFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {
3337
createFromReadableStream={createFromReadableStream}
3438
/>
3539
</StrictMode>,
40+
{
41+
// @ts-expect-error - no types for this yet
42+
formState,
43+
},
3644
);
3745
});
3846
});

packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
createTemporaryReferenceSet,
33
decodeAction,
4+
decodeFormState,
45
decodeReply,
56
loadServerAction,
67
renderToReadableStream,
@@ -12,8 +13,9 @@ import routes from "virtual:react-router/unstable_rsc/routes";
1213
export async function fetchServer(request: Request) {
1314
return await matchRSCServerRequest({
1415
createTemporaryReferenceSet,
15-
decodeReply,
1616
decodeAction,
17+
decodeFormState,
18+
decodeReply,
1719
loadServerAction,
1820
request,
1921
routes,

0 commit comments

Comments
 (0)