Skip to content

Commit c430329

Browse files
authored
fix: properly handle ?index on fetcher get submissions (#9312)
1 parent 8b00e7a commit c430329

File tree

4 files changed

+175
-2
lines changed

4 files changed

+175
-2
lines changed

.changeset/pretty-ravens-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
fix: properly handle ?index on fetcher get submissions (#9312)

packages/react-router-dom/__tests__/data-browser-router-test.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,93 @@ function testDomRouter(
22702270
`);
22712271
});
22722272

2273+
it("handles fetcher ?index params", async () => {
2274+
let { container } = render(
2275+
<TestDataRouter
2276+
window={getWindow("/parent")}
2277+
hydrationData={{ loaderData: { parent: null, index: null } }}
2278+
>
2279+
<Route
2280+
path="/parent"
2281+
element={<Outlet />}
2282+
action={() => "PARENT ACTION"}
2283+
loader={() => "PARENT LOADER"}
2284+
>
2285+
<Route
2286+
index
2287+
element={<Index />}
2288+
action={() => "INDEX ACTION"}
2289+
loader={() => "INDEX LOADER"}
2290+
/>
2291+
</Route>
2292+
</TestDataRouter>
2293+
);
2294+
2295+
function Index() {
2296+
let fetcher = useFetcher();
2297+
2298+
return (
2299+
<>
2300+
<p id="output">{fetcher.data}</p>
2301+
<button onClick={() => fetcher.load("/parent")}>
2302+
Load parent
2303+
</button>
2304+
<button onClick={() => fetcher.load("/parent?index")}>
2305+
Load index
2306+
</button>
2307+
<button onClick={() => fetcher.submit({})}>Submit empty</button>
2308+
<button
2309+
onClick={() =>
2310+
fetcher.submit({}, { method: "get", action: "/parent" })
2311+
}
2312+
>
2313+
Submit parent get
2314+
</button>
2315+
<button
2316+
onClick={() =>
2317+
fetcher.submit({}, { method: "get", action: "/parent?index" })
2318+
}
2319+
>
2320+
Submit index get
2321+
</button>
2322+
<button
2323+
onClick={() =>
2324+
fetcher.submit({}, { method: "post", action: "/parent" })
2325+
}
2326+
>
2327+
Submit parent post
2328+
</button>
2329+
<button
2330+
onClick={() =>
2331+
fetcher.submit(
2332+
{},
2333+
{ method: "post", action: "/parent?index" }
2334+
)
2335+
}
2336+
>
2337+
Submit index post
2338+
</button>
2339+
</>
2340+
);
2341+
}
2342+
2343+
async function clickAndAssert(btnText: string, expectedOutput: string) {
2344+
fireEvent.click(screen.getByText(btnText));
2345+
await waitFor(() => screen.getByText(new RegExp(expectedOutput)));
2346+
expect(getHtml(container.querySelector("#output"))).toContain(
2347+
expectedOutput
2348+
);
2349+
}
2350+
2351+
await clickAndAssert("Load parent", "PARENT LOADER");
2352+
await clickAndAssert("Load index", "INDEX LOADER");
2353+
await clickAndAssert("Submit empty", "INDEX LOADER");
2354+
await clickAndAssert("Submit parent get", "PARENT LOADER");
2355+
await clickAndAssert("Submit index get", "INDEX LOADER");
2356+
await clickAndAssert("Submit parent post", "PARENT ACTION");
2357+
await clickAndAssert("Submit index post", "INDEX ACTION");
2358+
});
2359+
22732360
it("handles fetcher.load errors", async () => {
22742361
let { container } = render(
22752362
<TestDataRouter

packages/router/__tests__/router-test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7605,6 +7605,76 @@ describe("a router", () => {
76057605
});
76067606
});
76077607
});
7608+
7609+
describe("fetcher ?index params", () => {
7610+
it("hits the proper Routes when ?index params are present", async () => {
7611+
let t = setup({
7612+
routes: [
7613+
{
7614+
id: "parent",
7615+
path: "parent",
7616+
action: true,
7617+
loader: true,
7618+
// Turn off revalidation after fetcher action submission for this test
7619+
shouldRevalidate: () => false,
7620+
children: [
7621+
{
7622+
id: "index",
7623+
index: true,
7624+
action: true,
7625+
loader: true,
7626+
// Turn off revalidation after fetcher action submission for this test
7627+
shouldRevalidate: () => false,
7628+
},
7629+
],
7630+
},
7631+
],
7632+
initialEntries: ["/parent"],
7633+
hydrationData: { loaderData: { parent: "PARENT", index: "INDEX" } },
7634+
});
7635+
7636+
let key = "KEY";
7637+
7638+
// fetcher.load()
7639+
let A = await t.fetch("/parent", key);
7640+
await A.loaders.parent.resolve("PARENT LOADER");
7641+
expect(t.router.getFetcher(key).data).toBe("PARENT LOADER");
7642+
7643+
let B = await t.fetch("/parent?index", key);
7644+
await B.loaders.index.resolve("INDEX LOADER");
7645+
expect(t.router.getFetcher(key).data).toBe("INDEX LOADER");
7646+
7647+
// fetcher.submit({}, { method: 'get' })
7648+
let C = await t.fetch("/parent", key, {
7649+
formMethod: "get",
7650+
formData: createFormData({}),
7651+
});
7652+
await C.loaders.parent.resolve("PARENT LOADER");
7653+
expect(t.router.getFetcher(key).data).toBe("PARENT LOADER");
7654+
7655+
let D = await t.fetch("/parent?index", key, {
7656+
formMethod: "get",
7657+
formData: createFormData({}),
7658+
});
7659+
await D.loaders.index.resolve("INDEX LOADER");
7660+
expect(t.router.getFetcher(key).data).toBe("INDEX LOADER");
7661+
7662+
// fetcher.submit({}, { method: 'post' })
7663+
let E = await t.fetch("/parent", key, {
7664+
formMethod: "post",
7665+
formData: createFormData({}),
7666+
});
7667+
await E.actions.parent.resolve("PARENT ACTION");
7668+
expect(t.router.getFetcher(key).data).toBe("PARENT ACTION");
7669+
7670+
let F = await t.fetch("/parent?index", key, {
7671+
formMethod: "post",
7672+
formData: createFormData({}),
7673+
});
7674+
await F.actions.index.resolve("INDEX ACTION");
7675+
expect(t.router.getFetcher(key).data).toBe("INDEX ACTION");
7676+
});
7677+
});
76087678
});
76097679

76107680
describe("deferred data", () => {

packages/router/router.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,7 +1165,7 @@ export function createRouter(init: RouterInit): Router {
11651165
return;
11661166
}
11671167

1168-
let { path, submission } = normalizeNavigateOptions(href, opts);
1168+
let { path, submission } = normalizeNavigateOptions(href, opts, true);
11691169
let match = getTargetMatch(matches, path);
11701170

11711171
if (submission) {
@@ -2098,7 +2098,8 @@ export function getStaticContextFromError(
20982098
// URLSearchParams so they behave identically to links with query params
20992099
function normalizeNavigateOptions(
21002100
to: To,
2101-
opts?: RouterNavigateOptions
2101+
opts?: RouterNavigateOptions,
2102+
isFetcher = false
21022103
): {
21032104
path: string;
21042105
submission?: Submission;
@@ -2134,6 +2135,16 @@ function normalizeNavigateOptions(
21342135
let parsedPath = parsePath(path);
21352136
try {
21362137
let searchParams = convertFormDataToSearchParams(opts.formData);
2138+
// Since fetcher GET submissions only run a single loader (as opposed to
2139+
// navigation GET submissions which run all loaders), we need to preserve
2140+
// any incoming ?index params
2141+
if (
2142+
isFetcher &&
2143+
parsedPath.search &&
2144+
hasNakedIndexQuery(parsedPath.search)
2145+
) {
2146+
searchParams.append("index", "");
2147+
}
21372148
parsedPath.search = `?${searchParams}`;
21382149
} catch (e) {
21392150
return {

0 commit comments

Comments
 (0)