Skip to content

Commit a1d9c74

Browse files
committed
support thrown redirects from server actions
1 parent c1f0dae commit a1d9c74

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

integration/rsc/rsc-test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,85 @@ implementations.forEach((implementation) => {
591591
// Ensure this is using RSC
592592
validateRSCHtml(await page.content());
593593
});
594+
595+
test("Supports React Server Functions thrown redirects", async ({
596+
page,
597+
}) => {
598+
let port = await getPort();
599+
stop = await setupRscTest({
600+
implementation,
601+
port,
602+
files: {
603+
"src/routes/home.actions.ts": js`
604+
"use server";
605+
import { redirect } from "react-router/rsc";
606+
607+
export function redirectAction(formData: FormData) {
608+
throw redirect("/?redirected=true");
609+
}
610+
`,
611+
"src/routes/home.client.tsx": js`
612+
"use client";
613+
import { useState } from "react";
614+
615+
export function Counter() {
616+
const [count, setCount] = useState(0);
617+
return <button type="button" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;
618+
}
619+
`,
620+
"src/routes/home.tsx": js`
621+
import { redirectAction } from "./home.actions";
622+
import { Counter } from "./home.client";
623+
624+
export default function ServerComponent(props) {
625+
console.log({props});
626+
return (
627+
<div>
628+
<form action={redirectAction}>
629+
<button type="submit" data-submit>
630+
Redirect via Server Function
631+
</button>
632+
</form>
633+
<Counter />
634+
</div>
635+
);
636+
}
637+
`,
638+
},
639+
});
640+
641+
await page.goto(`http://localhost:${port}/`);
642+
643+
// Verify initial server render
644+
await page.waitForSelector("[data-count]");
645+
expect(await page.locator("[data-count]").textContent()).toBe(
646+
"Count: 0"
647+
);
648+
await page.click("[data-count]");
649+
expect(await page.locator("[data-count]").textContent()).toBe(
650+
"Count: 1"
651+
);
652+
653+
// Submit the form to trigger server function redirect
654+
await page.click("[data-submit]");
655+
656+
await expect(page).toHaveURL(
657+
`http://localhost:${port}/?redirected=true`
658+
);
659+
660+
// Validate things are still interactive after redirect
661+
await page.click("[data-count]");
662+
expect(await page.locator("[data-count]").textContent()).toBe(
663+
"Count: 2"
664+
);
665+
await page.click("[data-count]");
666+
expect(await page.locator("[data-count]").textContent()).toBe(
667+
"Count: 3"
668+
);
669+
670+
// Ensure this is using RSC
671+
validateRSCHtml(await page.content());
672+
});
594673
});
595674

596675
test.describe("Errors", () => {

packages/react-router/lib/rsc/browser.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ declare global {
4545
}
4646
}
4747

48+
const neverResolvedPromise = new Promise<never>(() => {});
4849
export function createCallServer({
4950
decode,
5051
encodeAction,
@@ -70,6 +71,17 @@ export function createCallServer({
7071
}
7172
const payload = await decode(response.body);
7273

74+
if (payload.type === "redirect") {
75+
if (payload.reload) {
76+
window.location.href = payload.location;
77+
return;
78+
}
79+
window.__router.navigate(payload.location, {
80+
replace: payload.replace,
81+
});
82+
return neverResolvedPromise;
83+
}
84+
7385
if (payload.type !== "action") {
7486
throw new Error("Unexpected payload type");
7587
}

packages/react-router/lib/rsc/server.rsc.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ async function processServerAction(
234234
decodeFormAction: DecodeFormActionFunction | undefined,
235235
onError: ((error: unknown) => void) | undefined
236236
): Promise<
237-
{ revalidationRequest: Request; actionResult?: Promise<unknown> } | undefined
237+
| { revalidationRequest: Request; actionResult?: Promise<unknown> }
238+
| Response
239+
| undefined
238240
> {
239241
const getRevalidationRequest = () =>
240242
new Request(request.url, {
@@ -261,6 +263,9 @@ async function processServerAction(
261263
// Wait for actions to finish regardless of state
262264
await actionResult;
263265
} catch (error) {
266+
if (isResponse(error)) {
267+
return error;
268+
}
264269
// The error is propagated to the client through the result promise in the stream
265270
onError?.(error);
266271
}
@@ -282,6 +287,9 @@ async function processServerAction(
282287
try {
283288
await action();
284289
} catch (error) {
290+
if (isResponse(error)) {
291+
return error;
292+
}
285293
onError?.(error);
286294
}
287295
return {
@@ -349,6 +357,9 @@ async function generateRenderResponse(
349357
decodeFormAction,
350358
onError
351359
);
360+
if (isResponse(result)) {
361+
return generateRedirectResponse(statusCode, result, generateResponse);
362+
}
352363
actionResult = result?.actionResult;
353364
request = result?.revalidationRequest ?? request;
354365
}

0 commit comments

Comments
 (0)