Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: properly handle ?index on fetcher get submissions #9312

Merged
merged 2 commits into from
Sep 20, 2022
Merged
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
5 changes: 5 additions & 0 deletions .changeset/pretty-ravens-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/router": patch
---

fix: properly handle ?index on fetcher get submissions (#9312)
87 changes: 87 additions & 0 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,93 @@ function testDomRouter(
`);
});

it("handles fetcher ?index params", async () => {
let { container } = render(
<TestDataRouter
window={getWindow("/parent")}
hydrationData={{ loaderData: { parent: null, index: null } }}
>
<Route
path="/parent"
element={<Outlet />}
action={() => "PARENT ACTION"}
loader={() => "PARENT LOADER"}
>
<Route
index
element={<Index />}
action={() => "INDEX ACTION"}
loader={() => "INDEX LOADER"}
/>
</Route>
</TestDataRouter>
);

function Index() {
let fetcher = useFetcher();

return (
<>
<p id="output">{fetcher.data}</p>
<button onClick={() => fetcher.load("/parent")}>
Load parent
</button>
<button onClick={() => fetcher.load("/parent?index")}>
Load index
</button>
<button onClick={() => fetcher.submit({})}>Submit empty</button>
<button
onClick={() =>
fetcher.submit({}, { method: "get", action: "/parent" })
}
>
Submit parent get
</button>
<button
onClick={() =>
fetcher.submit({}, { method: "get", action: "/parent?index" })
}
>
Submit index get
</button>
<button
onClick={() =>
fetcher.submit({}, { method: "post", action: "/parent" })
}
>
Submit parent post
</button>
<button
onClick={() =>
fetcher.submit(
{},
{ method: "post", action: "/parent?index" }
)
}
>
Submit index post
</button>
</>
);
}

async function clickAndAssert(btnText: string, expectedOutput: string) {
fireEvent.click(screen.getByText(btnText));
await waitFor(() => screen.getByText(new RegExp(expectedOutput)));
expect(getHtml(container.querySelector("#output"))).toContain(
expectedOutput
);
}

await clickAndAssert("Load parent", "PARENT LOADER");
await clickAndAssert("Load index", "INDEX LOADER");
await clickAndAssert("Submit empty", "INDEX LOADER");
await clickAndAssert("Submit parent get", "PARENT LOADER");
await clickAndAssert("Submit index get", "INDEX LOADER");
await clickAndAssert("Submit parent post", "PARENT ACTION");
await clickAndAssert("Submit index post", "INDEX ACTION");
});

it("handles fetcher.load errors", async () => {
let { container } = render(
<TestDataRouter
Expand Down
70 changes: 70 additions & 0 deletions packages/router/__tests__/router-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7605,6 +7605,76 @@ describe("a router", () => {
});
});
});

describe("fetcher ?index params", () => {
it("hits the proper Routes when ?index params are present", async () => {
let t = setup({
routes: [
{
id: "parent",
path: "parent",
action: true,
loader: true,
// Turn off revalidation after fetcher action submission for this test
shouldRevalidate: () => false,
children: [
{
id: "index",
index: true,
action: true,
loader: true,
// Turn off revalidation after fetcher action submission for this test
shouldRevalidate: () => false,
},
],
},
],
initialEntries: ["/parent"],
hydrationData: { loaderData: { parent: "PARENT", index: "INDEX" } },
});

let key = "KEY";

// fetcher.load()
let A = await t.fetch("/parent", key);
await A.loaders.parent.resolve("PARENT LOADER");
expect(t.router.getFetcher(key).data).toBe("PARENT LOADER");

let B = await t.fetch("/parent?index", key);
await B.loaders.index.resolve("INDEX LOADER");
expect(t.router.getFetcher(key).data).toBe("INDEX LOADER");

// fetcher.submit({}, { method: 'get' })
let C = await t.fetch("/parent", key, {
formMethod: "get",
formData: createFormData({}),
});
await C.loaders.parent.resolve("PARENT LOADER");
expect(t.router.getFetcher(key).data).toBe("PARENT LOADER");

let D = await t.fetch("/parent?index", key, {
formMethod: "get",
formData: createFormData({}),
});
await D.loaders.index.resolve("INDEX LOADER");
expect(t.router.getFetcher(key).data).toBe("INDEX LOADER");

// fetcher.submit({}, { method: 'post' })
let E = await t.fetch("/parent", key, {
formMethod: "post",
formData: createFormData({}),
});
await E.actions.parent.resolve("PARENT ACTION");
expect(t.router.getFetcher(key).data).toBe("PARENT ACTION");

let F = await t.fetch("/parent?index", key, {
formMethod: "post",
formData: createFormData({}),
});
await F.actions.index.resolve("INDEX ACTION");
expect(t.router.getFetcher(key).data).toBe("INDEX ACTION");
});
});
});

describe("deferred data", () => {
Expand Down
15 changes: 13 additions & 2 deletions packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ export function createRouter(init: RouterInit): Router {
return;
}

let { path, submission } = normalizeNavigateOptions(href, opts);
let { path, submission } = normalizeNavigateOptions(href, opts, true);
let match = getTargetMatch(matches, path);

if (submission) {
Expand Down Expand Up @@ -2098,7 +2098,8 @@ export function getStaticContextFromError(
// URLSearchParams so they behave identically to links with query params
function normalizeNavigateOptions(
to: To,
opts?: RouterNavigateOptions
opts?: RouterNavigateOptions,
isFetcher = false
): {
path: string;
submission?: Submission;
Expand Down Expand Up @@ -2134,6 +2135,16 @@ function normalizeNavigateOptions(
let parsedPath = parsePath(path);
try {
let searchParams = convertFormDataToSearchParams(opts.formData);
// Since fetcher GET submissions only run a single loader (as opposed to
// navigation GET submissions which run all loaders), we need to preserve
// any incoming ?index params
if (
isFetcher &&
parsedPath.search &&
hasNakedIndexQuery(parsedPath.search)
) {
searchParams.append("index", "");
}
parsedPath.search = `?${searchParams}`;
} catch (e) {
return {
Expand Down