Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/early-doors-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-router/dev": patch
"react-router": patch
---

add support for throwing redirect Response's at RSC render time
3 changes: 2 additions & 1 deletion integration/helpers/rsc-parcel/src/prerender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ export async function prerender(
// Provide the React Server touchpoints.
createFromReadableStream,
// Render the router to HTML.
async renderHTML(getPayload) {
async renderHTML(getPayload, options) {
const payload = getPayload();

return await renderHTMLToReadableStream(
<RSCStaticRouter getPayload={getPayload} />,
{
...options,
bootstrapScriptContent,
formState: await payload.formState,
},
Expand Down
3 changes: 2 additions & 1 deletion integration/helpers/rsc-vite/src/entry.ssr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ export default async function handler(
request,
serverResponse,
createFromReadableStream,
async renderHTML(getPayload) {
async renderHTML(getPayload, options) {
const payload = getPayload();

return ReactDomServer.renderToReadableStream(
<RSCStaticRouter getPayload={getPayload} />,
{
...options,
bootstrapScriptContent,
signal: request.signal,
formState: await payload.formState,
Expand Down
129 changes: 127 additions & 2 deletions integration/rsc/rsc-nojs-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import getPort from "get-port";

import { implementations, js, setupRscTest, validateRSCHtml } from "./utils";

test.use({ javaScriptEnabled: false });

implementations.forEach((implementation) => {
test.describe(`RSC nojs (${implementation.name})`, () => {
let port: number;
Expand All @@ -20,6 +18,34 @@ implementations.forEach((implementation) => {
implementation,
port,
files: {
"src/routes.ts": js`
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";

export const routes = [
{
id: "root",
path: "",
lazy: () => import("./routes/root"),
children: [
{
id: "home",
index: true,
lazy: () => import("./routes/home"),
},
{
id: "render-redirect-lazy",
path: "/render-redirect/lazy/:id?",
lazy: () => import("./routes/render-redirect/lazy"),
},
{
id: "render-redirect",
path: "/render-redirect/:id?",
lazy: () => import("./routes/render-redirect/home"),
},
],
},
] satisfies RSCRouteConfig;
`,
"src/routes/home.actions.ts": js`
"use server";
import { redirect } from "react-router";
Expand Down Expand Up @@ -76,6 +102,60 @@ implementations.forEach((implementation) => {
);
}
`,

"src/routes/render-redirect/home.tsx": js`
import { Link, redirect } from "react-router";

export default function RenderRedirect({ params: { id } }) {
if (id === "redirect") {
throw redirect("/render-redirect/redirected");
}

if (id === "external") {
throw redirect("https://example.com/");
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
</>
)
}
`,
"src/routes/render-redirect/lazy.tsx": js`
import { Suspense } from "react";
import { Link, redirect } from "react-router";

export default function RenderRedirect({ params: { id } }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<Lazy id={id} />
</Suspense>
);
}

async function Lazy({ id }) {
await new Promise((r) => setTimeout(r, 0));

if (id === "redirect") {
throw redirect("/render-redirect/lazy/redirected");
}

if (id === "external") {
throw redirect("https://example.com/");
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
</>
);
}
`,
},
});
});
Expand Down Expand Up @@ -129,5 +209,50 @@ implementations.forEach((implementation) => {
// Ensure this is using RSC
validateRSCHtml(await page.content());
});

test("Suppport throwing redirect Response from render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("Redirect").click();
await page.waitForURL(
`http://localhost:${port}/render-redirect/redirected`,
);
await expect(page.getByText("redirected")).toBeAttached();
});

test("Suppport throwing external redirect Response from render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("External").click();
await page.waitForURL(`https://example.com/`);
await expect(page.getByText("Example Domain")).toBeAttached();
});

test("Suppport throwing redirect Response from suspended render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect/lazy/redirect`);
await page.waitForURL(
`http://localhost:${port}/render-redirect/lazy/redirected`,
);
await expect(page.getByText("redirected")).toBeAttached();
});

test("Suppport throwing external redirect Response from suspended render", async ({
page,
browserName,
}) => {
test.skip(
browserName === "firefox",
"Playwright doesn't like external meta redirects for tests. It times out waiting for the URL even though it navigates.",
);
await page.goto(`http://localhost:${port}/render-redirect/lazy/external`);
await page.waitForURL(`https://example.com/`);
await expect(page.getByText("Example Domain")).toBeAttached();
});
});
});
110 changes: 109 additions & 1 deletion integration/rsc/rsc-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,17 @@ implementations.forEach((implementation) => {
id: "action-transition-state",
path: "action-transition-state",
lazy: () => import("./routes/action-transition-state/home"),
}
},
{
id: "render-redirect-lazy",
path: "/render-redirect/lazy/:id?",
lazy: () => import("./routes/render-redirect/lazy"),
},
{
id: "render-redirect",
path: "/render-redirect/:id?",
lazy: () => import("./routes/render-redirect/home"),
},
],
},
] satisfies RSCRouteConfig;
Expand Down Expand Up @@ -1460,6 +1470,60 @@ implementations.forEach((implementation) => {
);
}
`,

"src/routes/render-redirect/home.tsx": js`
import { Link, redirect } from "react-router";

export default function RenderRedirect({ params: { id } }) {
if (id === "redirect") {
throw redirect("/render-redirect/redirected");
}

if (id === "external") {
throw redirect("https://example.com/")
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
</>
)
}
`,
"src/routes/render-redirect/lazy.tsx": js`
import { Suspense } from "react";
import { Link, redirect } from "react-router";

export default function RenderRedirect({ params: { id } }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<Lazy id={id} />
</Suspense>
);
}

async function Lazy({ id }) {
await new Promise((r) => setTimeout(r, 0));

if (id === "redirect") {
throw redirect("/render-redirect/lazy/redirected");
}

if (id === "external") {
throw redirect("https://example.com/")
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
</>
);
}
`,
},
});
});
Expand Down Expand Up @@ -1738,6 +1802,50 @@ implementations.forEach((implementation) => {
"An error occurred in the Server Components render.",
);
});

test("Suppport throwing redirect Response from render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("Redirect").click();
await page.waitForURL(
`http://localhost:${port}/render-redirect/redirected`,
);
await expect(page.getByText("redirected")).toBeAttached();
});

test("Suppport throwing external redirect Response from render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("External").click();
await page.waitForURL(`https://example.com/`);
await expect(page.getByText("Example Domain")).toBeAttached();
});

test("Suppport throwing redirect Response from suspended render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect/lazy`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("Redirect").click();
await page.waitForURL(
`http://localhost:${port}/render-redirect/lazy/redirected`,
);
await expect(page.getByText("redirected")).toBeAttached();
});

test("Suppport throwing external redirect Response from suspended render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect/lazy`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("External").click();
await page.waitForURL(`https://example.com/`);
await expect(page.getByText("Example Domain")).toBeAttached();
});
});

test.describe("Server Actions", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ export default async function handler(
request,
serverResponse,
createFromReadableStream,
async renderHTML(getPayload) {
async renderHTML(getPayload, options) {
const payload = getPayload();

return ReactDomServer.renderToReadableStream(
<RSCStaticRouter getPayload={getPayload} />,
{
...options,
bootstrapScriptContent,
signal: request.signal,
formState: await payload.formState,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-router/lib/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
NavigationContext,
RouteContext,
ViewTransitionContext,
useIsRSCRouterContext,
} from "./context";
import {
_renderMatches,
Expand Down Expand Up @@ -464,6 +465,9 @@ export function RouterProvider({
unstable_onError,
unstable_useTransitions,
}: RouterProviderProps): React.ReactElement {
let unstable_rsc = useIsRSCRouterContext();
unstable_useTransitions = unstable_rsc || unstable_useTransitions;

let [_state, setStateImpl] = React.useState(router.state);
let [state, setOptimisticState] = useOptimisticSafe(_state);
let [pendingState, setPendingState] = React.useState<RouterState>();
Expand Down
Loading