diff --git a/.changeset/sour-roses-heal.md b/.changeset/sour-roses-heal.md new file mode 100644 index 00000000000..f55b6553a80 --- /dev/null +++ b/.changeset/sour-roses-heal.md @@ -0,0 +1,5 @@ +--- +"@remix-run/react": patch +--- + +fix: preserve ?index for fetcher get submissions (#4238) diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts index 950a8ee758b..39e6534a06b 100644 --- a/integration/fetcher-test.ts +++ b/integration/fetcher-test.ts @@ -10,6 +10,10 @@ test.describe("useFetcher", () => { let CHEESESTEAK = "CHEESESTEAK"; let LUNCH = "LUNCH"; + let PARENT_LAYOUT_LOADER = "parent layout loader"; + let PARENT_LAYOUT_ACTION = "parent layout action"; + let PARENT_INDEX_LOADER = "parent index loader"; + let PARENT_INDEX_ACTION = "parent index action"; test.beforeAll(async () => { fixture = await createFixture({ @@ -80,6 +84,65 @@ test.describe("useFetcher", () => { ); } `, + + "app/routes/parent.jsx": js` + import { Outlet } from "@remix-run/react"; + + export function action() { + return "${PARENT_LAYOUT_ACTION}"; + }; + + export function loader() { + return "${PARENT_LAYOUT_LOADER}"; + }; + + export default function Parent() { + return ; + } + `, + + "app/routes/parent/index.jsx": js` + import { useFetcher } from "@remix-run/react"; + + export function action() { + return "${PARENT_INDEX_ACTION}"; + }; + + export function loader() { + return "${PARENT_INDEX_LOADER}"; + }; + + export default function ParentIndex() { + let fetcher = useFetcher(); + + return ( + <> +
{fetcher.data}
+ + + + + + + + + ); + } + `, }, }); @@ -142,4 +205,31 @@ test.describe("useFetcher", () => { await app.clickElement("#fetcher-submit"); expect(await app.getHtml("pre")).toMatch(CHEESESTEAK); }); + + test("fetchers handle ?index param correctly", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent"); + + await app.clickElement("#load-parent"); + expect(await app.getHtml("pre")).toMatch(PARENT_LAYOUT_LOADER); + + await app.clickElement("#load-index"); + expect(await app.getHtml("pre")).toMatch(PARENT_INDEX_LOADER); + + // fetcher.submit({}) defaults to GET for the current Route + await app.clickElement("#submit-empty"); + expect(await app.getHtml("pre")).toMatch(PARENT_INDEX_LOADER); + + await app.clickElement("#submit-parent-get"); + expect(await app.getHtml("pre")).toMatch(PARENT_LAYOUT_LOADER); + + await app.clickElement("#submit-index-get"); + expect(await app.getHtml("pre")).toMatch(PARENT_INDEX_LOADER); + + await app.clickElement("#submit-parent-post"); + expect(await app.getHtml("pre")).toMatch(PARENT_LAYOUT_ACTION); + + await app.clickElement("#submit-index-post"); + expect(await app.getHtml("pre")).toMatch(PARENT_INDEX_ACTION); + }); }); diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index 16641edf33c..0b39228f3b4 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -1233,6 +1233,16 @@ export function useSubmitImpl(key?: string): SubmitFunction { throw new Error(`Cannot submit binary form data using GET`); } } + + // Preserve any incoming ?index param for fetcher GET submissions + let isIndexAction = new URLSearchParams(url.search) + .getAll("index") + .some((v) => v === ""); + if (key != null && isIndexAction) { + hasParams = true; + params.append("index", ""); + } + url.search = hasParams ? `?${params.toString()}` : ""; }