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
{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()}` : ""; }