diff --git a/integration/helpers/rsc-parcel/src/entry.browser.tsx b/integration/helpers/rsc-parcel/src/entry.browser.tsx index af2621c657..d604446534 100644 --- a/integration/helpers/rsc-parcel/src/entry.browser.tsx +++ b/integration/helpers/rsc-parcel/src/entry.browser.tsx @@ -38,3 +38,13 @@ createFromReadableStream(getServerStream(), { assets: "manifest" }).then( }); } ); + +if (process.env.NODE_ENV !== "production") { + const ogError = console.error.bind(console); + console.error = (...args) => { + if (args[1] === Symbol.for("react-router.redirect")) { + return; + } + ogError(...args); + }; +} diff --git a/integration/rsc/rsc-test.ts b/integration/rsc/rsc-test.ts index df0de52e06..5233aa1d3b 100644 --- a/integration/rsc/rsc-test.ts +++ b/integration/rsc/rsc-test.ts @@ -461,7 +461,10 @@ implementations.forEach((implementation) => { validateRSCHtml(await page.content()); }); - test("Supports resource routes as URL and fetchers", async ({ page }) => { + test("Supports resource routes as URL and fetchers", async ({ + page, + request, + }) => { let port = await getPort(); stop = await setupRscTest({ implementation, @@ -481,19 +484,24 @@ implementations.forEach((implementation) => { index: true, lazy: () => import("./routes/home"), }, - { - id: "resource", - path: "resource", - lazy: () => import("./routes/resource"), - }, ], }, + { + id: "resource", + path: "resource", + lazy: () => import("./routes/resource"), + }, ] satisfies ServerRouteObject[]; `, "src/routes/resource.tsx": js` export function loader() { + return Response.json({ message: "Hello from resource route!" }); + } + + export async function action({ request }) { return { message: "Hello from resource route!", + echo: await request.text(), }; } `, @@ -506,7 +514,7 @@ implementations.forEach((implementation) => { const fetcher = useFetcher(); const loadResource = () => { - fetcher.load("/resource"); + fetcher.submit({ hello: "world" }, { method: "post", action: "/resource" }); }; return ( @@ -532,19 +540,38 @@ implementations.forEach((implementation) => { `, }, }); - const response = await page.goto(`http://localhost:${port}/resource`); - expect(response?.status()).toBe(200); - expect(await response?.json()).toEqual({ + const getResponse = await request.get( + `http://localhost:${port}/resource` + ); + expect(getResponse?.status()).toBe(200); + expect(await getResponse?.json()).toEqual({ message: "Hello from resource route!", }); + const postResponse = await request.post( + `http://localhost:${port}/resource`, + { + data: { hello: "world" }, + } + ); + expect(postResponse?.status()).toBe(200); + expect(await postResponse?.json()).toEqual({ + message: "Hello from resource route!", + echo: JSON.stringify({ hello: "world" }), + }); + await page.goto(`http://localhost:${port}/`); await page.click("button"); await page.waitForSelector("[data-testid=resource-data]"); expect( await page.locator("[data-testid=resource-data]").textContent() - ).toBe(JSON.stringify({ message: "Hello from resource route!" })); + ).toBe( + JSON.stringify({ + message: "Hello from resource route!", + echo: "hello=world", + }) + ); }); }); diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index 8f01fec1bf..66c849c4e8 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -297,6 +297,9 @@ async function processServerAction( signal: request.signal, }); + const isFormRequest = canDecodeWithFormData( + request.headers.get("Content-Type") + ); const actionId = request.headers.get("rsc-action-id"); if (actionId) { if (!decodeCallServer) { @@ -305,7 +308,7 @@ async function processServerAction( ); } - const reply = canDecodeWithFormData(request.headers.get("Content-Type")) + const reply = isFormRequest ? await request.formData() : await request.text(); const serverAction = await decodeCallServer(actionId, reply); @@ -325,32 +328,28 @@ async function processServerAction( actionResult, revalidationRequest: getRevalidationRequest(), }; - } - - const clone = request.clone(); - const formData = await request.formData(); - if (Array.from(formData.keys()).some((k) => k.startsWith("$ACTION_"))) { - if (!decodeFormAction) { - throw new Error( - "Cannot handle form actions without a decodeFormAction function" - ); - } - const action = await decodeFormAction(formData); - try { - await action(); - } catch (error) { - if (isResponse(error)) { - return error; + } else if (isFormRequest) { + const formData = await request.clone().formData(); + if (Array.from(formData.keys()).some((k) => k.startsWith("$ACTION_"))) { + if (!decodeFormAction) { + throw new Error( + "Cannot handle form actions without a decodeFormAction function" + ); } - onError?.(error); + const action = await decodeFormAction(formData); + try { + await action(); + } catch (error) { + if (isResponse(error)) { + return error; + } + onError?.(error); + } + return { + revalidationRequest: getRevalidationRequest(), + }; } - return { - revalidationRequest: getRevalidationRequest(), - }; } - return { - revalidationRequest: clone, - }; } async function generateResourceResponse( diff --git a/playground/rsc-parcel/src/entry.browser.tsx b/playground/rsc-parcel/src/entry.browser.tsx index f8a39fb5a7..d772ba1aa9 100644 --- a/playground/rsc-parcel/src/entry.browser.tsx +++ b/playground/rsc-parcel/src/entry.browser.tsx @@ -39,3 +39,13 @@ createFromReadableStream(getServerStream(), { assets: "manifest" }).then( }); } ); + +if (process.env.NODE_ENV !== "production") { + const ogError = console.error.bind(console); + console.error = (...args) => { + if (args[1] === Symbol.for("react-router.redirect")) { + return; + } + ogError(...args); + }; +} diff --git a/playground/rsc-parcel/src/routes/fetcher/fetcher.client.tsx b/playground/rsc-parcel/src/routes/fetcher/fetcher.client.tsx index 2c48ea8963..dcfdcbc33d 100644 --- a/playground/rsc-parcel/src/routes/fetcher/fetcher.client.tsx +++ b/playground/rsc-parcel/src/routes/fetcher/fetcher.client.tsx @@ -1,14 +1,21 @@ "use client"; import { useFetcher } from "react-router"; -import type { loader } from "../resource/resource"; +import type { action } from "../resource/resource"; export default function FetcherRoute() { - const fetcher = useFetcher(); + const fetcher = useFetcher(); return (
-
{JSON.stringify(fetcher.data, null, 2)}
diff --git a/playground/rsc-parcel/src/routes/home/home.actions.ts b/playground/rsc-parcel/src/routes/home/home.actions.ts new file mode 100644 index 0000000000..1f3d45d715 --- /dev/null +++ b/playground/rsc-parcel/src/routes/home/home.actions.ts @@ -0,0 +1,7 @@ +"use server"; + +import { redirect } from "react-router/rsc"; + +export function redirectAction(formData: FormData) { + throw redirect("/?redirected=true"); +} diff --git a/playground/rsc-parcel/src/routes/home/home.client.tsx b/playground/rsc-parcel/src/routes/home/home.client.tsx index 11277d9199..0b83ab1fef 100644 --- a/playground/rsc-parcel/src/routes/home/home.client.tsx +++ b/playground/rsc-parcel/src/routes/home/home.client.tsx @@ -5,6 +5,7 @@ import { type ClientLoaderFunctionArgs, useLoaderData } from "react-router"; import { Counter } from "../../counter"; import type { loader } from "./home"; +import { redirectAction } from "./home.actions"; export async function clientLoader({ serverLoader }: ClientLoaderFunctionArgs) { const res = await serverLoader(); @@ -23,6 +24,11 @@ export default function HomeRoute() {

{message}

Did client loader run? {client ? "Yes" : "No"}

+
+ +
); } diff --git a/playground/rsc-parcel/src/routes/resource/resource.ts b/playground/rsc-parcel/src/routes/resource/resource.ts index 188f306ca6..6d52c79aa6 100644 --- a/playground/rsc-parcel/src/routes/resource/resource.ts +++ b/playground/rsc-parcel/src/routes/resource/resource.ts @@ -1,6 +1,9 @@ -export function loader() { +import type { ActionFunctionArgs } from "react-router"; + +export async function action({ request }: ActionFunctionArgs) { return { timestamp: Date.now(), message: "Hello from resource route!", + echo: await request.text(), }; }