diff --git a/integration/rsc/rsc-test.ts b/integration/rsc/rsc-test.ts
index e13e185763..10f53593fa 100644
--- a/integration/rsc/rsc-test.ts
+++ b/integration/rsc/rsc-test.ts
@@ -591,6 +591,85 @@ implementations.forEach((implementation) => {
// Ensure this is using RSC
validateRSCHtml(await page.content());
});
+
+ test("Supports React Server Functions thrown redirects", async ({
+ page,
+ }) => {
+ let port = await getPort();
+ stop = await setupRscTest({
+ implementation,
+ port,
+ files: {
+ "src/routes/home.actions.ts": js`
+ "use server";
+ import { redirect } from "react-router/rsc";
+
+ export function redirectAction(formData: FormData) {
+ throw redirect("/?redirected=true");
+ }
+ `,
+ "src/routes/home.client.tsx": js`
+ "use client";
+ import { useState } from "react";
+
+ export function Counter() {
+ const [count, setCount] = useState(0);
+ return ;
+ }
+ `,
+ "src/routes/home.tsx": js`
+ import { redirectAction } from "./home.actions";
+ import { Counter } from "./home.client";
+
+ export default function ServerComponent(props) {
+ console.log({props});
+ return (
+
+
+
+
+ );
+ }
+ `,
+ },
+ });
+
+ await page.goto(`http://localhost:${port}/`);
+
+ // Verify initial server render
+ await page.waitForSelector("[data-count]");
+ expect(await page.locator("[data-count]").textContent()).toBe(
+ "Count: 0"
+ );
+ await page.click("[data-count]");
+ expect(await page.locator("[data-count]").textContent()).toBe(
+ "Count: 1"
+ );
+
+ // Submit the form to trigger server function redirect
+ await page.click("[data-submit]");
+
+ await expect(page).toHaveURL(
+ `http://localhost:${port}/?redirected=true`
+ );
+
+ // Validate things are still interactive after redirect
+ await page.click("[data-count]");
+ expect(await page.locator("[data-count]").textContent()).toBe(
+ "Count: 2"
+ );
+ await page.click("[data-count]");
+ expect(await page.locator("[data-count]").textContent()).toBe(
+ "Count: 3"
+ );
+
+ // Ensure this is using RSC
+ validateRSCHtml(await page.content());
+ });
});
test.describe("Errors", () => {
diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx
index 6d8f5a3fe3..a6fdf58e8a 100644
--- a/packages/react-router/lib/rsc/browser.tsx
+++ b/packages/react-router/lib/rsc/browser.tsx
@@ -45,6 +45,7 @@ declare global {
}
}
+const neverResolvedPromise = new Promise(() => {});
export function createCallServer({
decode,
encodeAction,
@@ -70,6 +71,17 @@ export function createCallServer({
}
const payload = await decode(response.body);
+ if (payload.type === "redirect") {
+ if (payload.reload) {
+ window.location.href = payload.location;
+ return;
+ }
+ window.__router.navigate(payload.location, {
+ replace: payload.replace,
+ });
+ return neverResolvedPromise;
+ }
+
if (payload.type !== "action") {
throw new Error("Unexpected payload type");
}
diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts
index e6b951f24a..169124cdcd 100644
--- a/packages/react-router/lib/rsc/server.rsc.ts
+++ b/packages/react-router/lib/rsc/server.rsc.ts
@@ -234,7 +234,9 @@ async function processServerAction(
decodeFormAction: DecodeFormActionFunction | undefined,
onError: ((error: unknown) => void) | undefined
): Promise<
- { revalidationRequest: Request; actionResult?: Promise } | undefined
+ | { revalidationRequest: Request; actionResult?: Promise }
+ | Response
+ | undefined
> {
const getRevalidationRequest = () =>
new Request(request.url, {
@@ -261,6 +263,9 @@ async function processServerAction(
// Wait for actions to finish regardless of state
await actionResult;
} catch (error) {
+ if (isResponse(error)) {
+ return error;
+ }
// The error is propagated to the client through the result promise in the stream
onError?.(error);
}
@@ -282,6 +287,9 @@ async function processServerAction(
try {
await action();
} catch (error) {
+ if (isResponse(error)) {
+ return error;
+ }
onError?.(error);
}
return {
@@ -349,6 +357,9 @@ async function generateRenderResponse(
decodeFormAction,
onError
);
+ if (isResponse(result)) {
+ return generateRedirectResponse(statusCode, result, generateResponse);
+ }
actionResult = result?.actionResult;
request = result?.revalidationRequest ?? request;
}