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; }