-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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 issues with contextual routing and index params #12003
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
"react-router-dom": patch | ||
"react-router": patch | ||
"@remix-run/router": patch | ||
--- | ||
|
||
- Fix bug when submitting to the current contextual route (parent route with an index child) when an `?index` param already exists from a prior submission | ||
- Fix `useFormAction` bug - when removing `?index` param it would not keep other non-Remix `index` params |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3000,6 +3000,280 @@ function testDomRouter( | |
}); | ||
}); | ||
|
||
describe("submitting to self from parent/index when ?index param exists", () => { | ||
it("useSubmit", async () => { | ||
let router = createTestRouter( | ||
createRoutesFromElements( | ||
<Route | ||
id="parent" | ||
path="/parent" | ||
element={<Parent />} | ||
action={({ request }) => "PARENT ACTION: " + request.url} | ||
> | ||
<Route | ||
id="index" | ||
index | ||
element={<Index />} | ||
action={({ request }) => "INDEX ACTION: " + request.url} | ||
/> | ||
</Route> | ||
), | ||
{ | ||
window: getWindow("/parent?index&index=keep"), | ||
} | ||
); | ||
let { container } = render(<RouterProvider router={router} />); | ||
|
||
function Parent() { | ||
let actionData = useActionData(); | ||
let submit = useSubmit(); | ||
return ( | ||
<> | ||
<p id="parent">{actionData}</p> | ||
<button onClick={() => submit({}, { method: "post" })}> | ||
Submit from parent | ||
</button> | ||
<Outlet /> | ||
</> | ||
); | ||
} | ||
|
||
function Index() { | ||
let actionData = useActionData(); | ||
let submit = useSubmit(); | ||
return ( | ||
<> | ||
<p id="index">{actionData}</p> | ||
<button onClick={() => submit({}, { method: "post" })}> | ||
Submit from index | ||
</button> | ||
</> | ||
); | ||
} | ||
|
||
fireEvent.click(screen.getByText("Submit from parent")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION"))); | ||
expect(getHtml(container.querySelector("#parent")!)).toContain( | ||
"PARENT ACTION: http://localhost/parent?index=keep" | ||
); | ||
|
||
fireEvent.click(screen.getByText("Submit from index")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION"))); | ||
expect(getHtml(container.querySelector("#index")!)).toContain( | ||
"INDEX ACTION: http://localhost/parent?index&index=keep" | ||
); | ||
}); | ||
|
||
it("Form", async () => { | ||
let router = createTestRouter( | ||
createRoutesFromElements( | ||
<Route | ||
id="parent" | ||
path="/parent" | ||
element={<Parent />} | ||
action={({ request }) => "PARENT ACTION: " + request.url} | ||
> | ||
<Route | ||
id="index" | ||
index | ||
element={<Index />} | ||
action={({ request }) => "INDEX ACTION: " + request.url} | ||
/> | ||
</Route> | ||
), | ||
{ | ||
window: getWindow("/parent?index&index=keep"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
); | ||
let { container } = render(<RouterProvider router={router} />); | ||
|
||
function Parent() { | ||
let actionData = useActionData(); | ||
return ( | ||
<> | ||
<p id="parent">{actionData}</p> | ||
<Form method="post" id="parent-form"> | ||
<button type="submit">Submit from parent</button> | ||
</Form> | ||
<Outlet /> | ||
</> | ||
); | ||
} | ||
|
||
function Index() { | ||
let actionData = useActionData(); | ||
return ( | ||
<> | ||
<p id="index">{actionData}</p> | ||
<Form method="post" id="index-form"> | ||
<button type="submit">Submit from index</button> | ||
</Form> | ||
</> | ||
); | ||
} | ||
|
||
expect( | ||
container.querySelector("#parent-form")?.getAttribute("action") | ||
).toBe("/parent?index=keep"); | ||
expect( | ||
container.querySelector("#index-form")?.getAttribute("action") | ||
).toBe("/parent?index&index=keep"); | ||
|
||
fireEvent.click(screen.getByText("Submit from parent")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION"))); | ||
expect(getHtml(container.querySelector("#parent")!)).toContain( | ||
"PARENT ACTION: http://localhost/parent?index=keep" | ||
); | ||
|
||
fireEvent.click(screen.getByText("Submit from index")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION"))); | ||
expect(getHtml(container.querySelector("#index")!)).toContain( | ||
"INDEX ACTION: http://localhost/parent?index&index=keep" | ||
); | ||
}); | ||
|
||
it("fetcher.submit", async () => { | ||
let router = createTestRouter( | ||
createRoutesFromElements( | ||
<Route | ||
id="parent" | ||
path="/parent" | ||
element={<Parent />} | ||
action={({ request }) => "PARENT ACTION: " + request.url} | ||
> | ||
<Route | ||
id="index" | ||
index | ||
element={<Index />} | ||
action={({ request }) => "INDEX ACTION: " + request.url} | ||
/> | ||
</Route> | ||
), | ||
{ | ||
window: getWindow("/parent?index&index=keep"), | ||
} | ||
); | ||
let { container } = render(<RouterProvider router={router} />); | ||
|
||
function Parent() { | ||
let fetcher = useFetcher(); | ||
|
||
return ( | ||
<> | ||
<p id="parent">{fetcher.data}</p> | ||
<button onClick={() => fetcher.submit({}, { method: "post" })}> | ||
Submit from parent | ||
</button> | ||
<Outlet /> | ||
</> | ||
); | ||
} | ||
|
||
function Index() { | ||
let fetcher = useFetcher(); | ||
|
||
return ( | ||
<> | ||
<p id="index">{fetcher.data}</p> | ||
<button onClick={() => fetcher.submit({}, { method: "post" })}> | ||
Submit from index | ||
</button> | ||
</> | ||
); | ||
} | ||
|
||
fireEvent.click(screen.getByText("Submit from parent")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION"))); | ||
expect(getHtml(container.querySelector("#parent")!)).toContain( | ||
"PARENT ACTION: http://localhost/parent?index=keep" | ||
); | ||
|
||
fireEvent.click(screen.getByText("Submit from index")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION"))); | ||
expect(getHtml(container.querySelector("#index")!)).toContain( | ||
"INDEX ACTION: http://localhost/parent?index&index=keep" | ||
); | ||
}); | ||
|
||
it("fetcher.Form", async () => { | ||
let router = createTestRouter( | ||
createRoutesFromElements( | ||
<Route | ||
id="parent" | ||
path="/parent" | ||
element={<Parent />} | ||
action={({ request }) => "PARENT ACTION: " + request.url} | ||
> | ||
<Route | ||
id="index" | ||
index | ||
element={<Index />} | ||
action={({ request }) => "INDEX ACTION: " + request.url} | ||
/> | ||
</Route> | ||
), | ||
{ | ||
window: getWindow("/parent?index&index=keep"), | ||
} | ||
); | ||
let { container } = render(<RouterProvider router={router} />); | ||
|
||
function Parent() { | ||
let fetcher = useFetcher(); | ||
|
||
return ( | ||
<> | ||
<p id="parent">{fetcher.data}</p> | ||
<fetcher.Form method="post" id="parent-form"> | ||
<button type="submit">Submit from parent</button> | ||
</fetcher.Form> | ||
<Outlet /> | ||
</> | ||
); | ||
} | ||
|
||
function Index() { | ||
let fetcher = useFetcher(); | ||
|
||
return ( | ||
<> | ||
<p id="index">{fetcher.data}</p> | ||
<fetcher.Form method="post" id="index-form"> | ||
<button type="submit">Submit from index</button> | ||
</fetcher.Form> | ||
</> | ||
); | ||
} | ||
|
||
expect( | ||
container.querySelector("#parent-form")?.getAttribute("action") | ||
).toBe("/parent?index=keep"); | ||
expect( | ||
container.querySelector("#index-form")?.getAttribute("action") | ||
).toBe("/parent?index&index=keep"); | ||
|
||
fireEvent.click(screen.getByText("Submit from parent")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION"))); | ||
expect(getHtml(container.querySelector("#parent")!)).toContain( | ||
"PARENT ACTION: http://localhost/parent?index=keep" | ||
); | ||
|
||
fireEvent.click(screen.getByText("Submit from index")); | ||
await tick(); | ||
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION"))); | ||
expect(getHtml(container.querySelector("#index")!)).toContain( | ||
"INDEX ACTION: http://localhost/parent?index&index=keep" | ||
); | ||
}); | ||
}); | ||
|
||
it("allows user to specify search params and hash", async () => { | ||
let router = createTestRouter( | ||
createRoutesFromElements( | ||
|
@@ -5370,9 +5644,9 @@ function testDomRouter( | |
let { container } = render(<RouterProvider router={router} />); | ||
expect(container.innerHTML).not.toMatch(/my-key/); | ||
await waitFor(() => | ||
// React `useId()` results in either `:r2a:` or `:rp:` depending on | ||
// `DataBrowserRouter`/`DataHashRouter` | ||
expect(container.innerHTML).toMatch(/(:r2a:|:rp:),my-key/) | ||
// React `useId()` results in something such as `:r2a:`, `:r2i:`, | ||
// `:rt:`, or `:rp:` depending on `DataBrowserRouter`/`DataHashRouter` | ||
expect(container.innerHTML).toMatch(/(:r[0-9]?[a-z]:),my-key/) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These internal react IDs seem to have changed with the new tests |
||
); | ||
}); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1635,9 +1635,13 @@ export function useFormAction( | |
// since it might not apply to our contextual route. We add it back based | ||
// on match.route.index below | ||
let params = new URLSearchParams(path.search); | ||
if (params.has("index") && params.get("index") === "") { | ||
let indexValues = params.getAll("index"); | ||
let hasNakedIndexParam = indexValues.some((v) => v === ""); | ||
if (hasNakedIndexParam) { | ||
params.delete("index"); | ||
path.search = params.toString() ? `?${params.toString()}` : ""; | ||
indexValues.filter((v) => v).forEach((v) => params.append("index", v)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve user-specified |
||
let qs = params.toString(); | ||
path.search = qs ? `?${qs}` : ""; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4169,16 +4169,23 @@ function normalizeTo( | |
path.hash = location.hash; | ||
} | ||
|
||
// Add an ?index param for matched index routes if we don't already have one | ||
if ( | ||
(to == null || to === "" || to === ".") && | ||
activeRouteMatch && | ||
activeRouteMatch.route.index && | ||
!hasNakedIndexQuery(path.search) | ||
) { | ||
path.search = path.search | ||
? path.search.replace(/^\?/, "?index&") | ||
: "?index"; | ||
// Account for `?index` params when routing to the current location | ||
if ((to == null || to === "" || to === ".") && activeRouteMatch) { | ||
let nakedIndex = hasNakedIndexQuery(path.search); | ||
if (activeRouteMatch.route.index && !nakedIndex) { | ||
// Add one when we're targeting an index route | ||
path.search = path.search | ||
? path.search.replace(/^\?/, "?index&") | ||
: "?index"; | ||
} else if (!activeRouteMatch.route.index && nakedIndex) { | ||
// Remove existing ones when we're not | ||
let params = new URLSearchParams(path.search); | ||
let indexValues = params.getAll("index"); | ||
params.delete("index"); | ||
indexValues.filter((v) => v).forEach((v) => params.append("index", v)); | ||
let qs = params.toString(); | ||
path.search = qs ? `?${qs}` : ""; | ||
Comment on lines
+4181
to
+4187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove |
||
} | ||
} | ||
|
||
// If we're operating within a basename, prepend it to the pathname. If | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we have an
?index
around from a prior submission, we were incorrectly letting that apply to asubmit(data, { method: 'post' })
in the parent component and then it would try to submit to the child index route action and not the proper contextual parent route actionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was handled properly in
useFormAction
(soForm
/fetcher.Form
didn't have the bug) but when usinguseSubmit
it goes straight to the router which does the path resolving, so we were missing the logic inrouter.navigate
/router.fetch