Skip to content

Commit 0389eba

Browse files
authored
Fix issues with contextual routing and index params (#12003)
1 parent f941ddf commit 0389eba

File tree

4 files changed

+308
-15
lines changed

4 files changed

+308
-15
lines changed

.changeset/calm-wombats-design.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"react-router-dom": patch
3+
"react-router": patch
4+
"@remix-run/router": patch
5+
---
6+
7+
- 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
8+
- Fix `useFormAction` bug - when removing `?index` param it would not keep other non-Remix `index` params

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

Lines changed: 277 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3000,6 +3000,280 @@ function testDomRouter(
30003000
});
30013001
});
30023002

3003+
describe("submitting to self from parent/index when ?index param exists", () => {
3004+
it("useSubmit", async () => {
3005+
let router = createTestRouter(
3006+
createRoutesFromElements(
3007+
<Route
3008+
id="parent"
3009+
path="/parent"
3010+
element={<Parent />}
3011+
action={({ request }) => "PARENT ACTION: " + request.url}
3012+
>
3013+
<Route
3014+
id="index"
3015+
index
3016+
element={<Index />}
3017+
action={({ request }) => "INDEX ACTION: " + request.url}
3018+
/>
3019+
</Route>
3020+
),
3021+
{
3022+
window: getWindow("/parent?index&index=keep"),
3023+
}
3024+
);
3025+
let { container } = render(<RouterProvider router={router} />);
3026+
3027+
function Parent() {
3028+
let actionData = useActionData();
3029+
let submit = useSubmit();
3030+
return (
3031+
<>
3032+
<p id="parent">{actionData}</p>
3033+
<button onClick={() => submit({}, { method: "post" })}>
3034+
Submit from parent
3035+
</button>
3036+
<Outlet />
3037+
</>
3038+
);
3039+
}
3040+
3041+
function Index() {
3042+
let actionData = useActionData();
3043+
let submit = useSubmit();
3044+
return (
3045+
<>
3046+
<p id="index">{actionData}</p>
3047+
<button onClick={() => submit({}, { method: "post" })}>
3048+
Submit from index
3049+
</button>
3050+
</>
3051+
);
3052+
}
3053+
3054+
fireEvent.click(screen.getByText("Submit from parent"));
3055+
await tick();
3056+
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION")));
3057+
expect(getHtml(container.querySelector("#parent")!)).toContain(
3058+
"PARENT ACTION: http://localhost/parent?index=keep"
3059+
);
3060+
3061+
fireEvent.click(screen.getByText("Submit from index"));
3062+
await tick();
3063+
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION")));
3064+
expect(getHtml(container.querySelector("#index")!)).toContain(
3065+
"INDEX ACTION: http://localhost/parent?index&index=keep"
3066+
);
3067+
});
3068+
3069+
it("Form", async () => {
3070+
let router = createTestRouter(
3071+
createRoutesFromElements(
3072+
<Route
3073+
id="parent"
3074+
path="/parent"
3075+
element={<Parent />}
3076+
action={({ request }) => "PARENT ACTION: " + request.url}
3077+
>
3078+
<Route
3079+
id="index"
3080+
index
3081+
element={<Index />}
3082+
action={({ request }) => "INDEX ACTION: " + request.url}
3083+
/>
3084+
</Route>
3085+
),
3086+
{
3087+
window: getWindow("/parent?index&index=keep"),
3088+
}
3089+
);
3090+
let { container } = render(<RouterProvider router={router} />);
3091+
3092+
function Parent() {
3093+
let actionData = useActionData();
3094+
return (
3095+
<>
3096+
<p id="parent">{actionData}</p>
3097+
<Form method="post" id="parent-form">
3098+
<button type="submit">Submit from parent</button>
3099+
</Form>
3100+
<Outlet />
3101+
</>
3102+
);
3103+
}
3104+
3105+
function Index() {
3106+
let actionData = useActionData();
3107+
return (
3108+
<>
3109+
<p id="index">{actionData}</p>
3110+
<Form method="post" id="index-form">
3111+
<button type="submit">Submit from index</button>
3112+
</Form>
3113+
</>
3114+
);
3115+
}
3116+
3117+
expect(
3118+
container.querySelector("#parent-form")?.getAttribute("action")
3119+
).toBe("/parent?index=keep");
3120+
expect(
3121+
container.querySelector("#index-form")?.getAttribute("action")
3122+
).toBe("/parent?index&index=keep");
3123+
3124+
fireEvent.click(screen.getByText("Submit from parent"));
3125+
await tick();
3126+
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION")));
3127+
expect(getHtml(container.querySelector("#parent")!)).toContain(
3128+
"PARENT ACTION: http://localhost/parent?index=keep"
3129+
);
3130+
3131+
fireEvent.click(screen.getByText("Submit from index"));
3132+
await tick();
3133+
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION")));
3134+
expect(getHtml(container.querySelector("#index")!)).toContain(
3135+
"INDEX ACTION: http://localhost/parent?index&index=keep"
3136+
);
3137+
});
3138+
3139+
it("fetcher.submit", async () => {
3140+
let router = createTestRouter(
3141+
createRoutesFromElements(
3142+
<Route
3143+
id="parent"
3144+
path="/parent"
3145+
element={<Parent />}
3146+
action={({ request }) => "PARENT ACTION: " + request.url}
3147+
>
3148+
<Route
3149+
id="index"
3150+
index
3151+
element={<Index />}
3152+
action={({ request }) => "INDEX ACTION: " + request.url}
3153+
/>
3154+
</Route>
3155+
),
3156+
{
3157+
window: getWindow("/parent?index&index=keep"),
3158+
}
3159+
);
3160+
let { container } = render(<RouterProvider router={router} />);
3161+
3162+
function Parent() {
3163+
let fetcher = useFetcher();
3164+
3165+
return (
3166+
<>
3167+
<p id="parent">{fetcher.data}</p>
3168+
<button onClick={() => fetcher.submit({}, { method: "post" })}>
3169+
Submit from parent
3170+
</button>
3171+
<Outlet />
3172+
</>
3173+
);
3174+
}
3175+
3176+
function Index() {
3177+
let fetcher = useFetcher();
3178+
3179+
return (
3180+
<>
3181+
<p id="index">{fetcher.data}</p>
3182+
<button onClick={() => fetcher.submit({}, { method: "post" })}>
3183+
Submit from index
3184+
</button>
3185+
</>
3186+
);
3187+
}
3188+
3189+
fireEvent.click(screen.getByText("Submit from parent"));
3190+
await tick();
3191+
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION")));
3192+
expect(getHtml(container.querySelector("#parent")!)).toContain(
3193+
"PARENT ACTION: http://localhost/parent?index=keep"
3194+
);
3195+
3196+
fireEvent.click(screen.getByText("Submit from index"));
3197+
await tick();
3198+
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION")));
3199+
expect(getHtml(container.querySelector("#index")!)).toContain(
3200+
"INDEX ACTION: http://localhost/parent?index&index=keep"
3201+
);
3202+
});
3203+
3204+
it("fetcher.Form", async () => {
3205+
let router = createTestRouter(
3206+
createRoutesFromElements(
3207+
<Route
3208+
id="parent"
3209+
path="/parent"
3210+
element={<Parent />}
3211+
action={({ request }) => "PARENT ACTION: " + request.url}
3212+
>
3213+
<Route
3214+
id="index"
3215+
index
3216+
element={<Index />}
3217+
action={({ request }) => "INDEX ACTION: " + request.url}
3218+
/>
3219+
</Route>
3220+
),
3221+
{
3222+
window: getWindow("/parent?index&index=keep"),
3223+
}
3224+
);
3225+
let { container } = render(<RouterProvider router={router} />);
3226+
3227+
function Parent() {
3228+
let fetcher = useFetcher();
3229+
3230+
return (
3231+
<>
3232+
<p id="parent">{fetcher.data}</p>
3233+
<fetcher.Form method="post" id="parent-form">
3234+
<button type="submit">Submit from parent</button>
3235+
</fetcher.Form>
3236+
<Outlet />
3237+
</>
3238+
);
3239+
}
3240+
3241+
function Index() {
3242+
let fetcher = useFetcher();
3243+
3244+
return (
3245+
<>
3246+
<p id="index">{fetcher.data}</p>
3247+
<fetcher.Form method="post" id="index-form">
3248+
<button type="submit">Submit from index</button>
3249+
</fetcher.Form>
3250+
</>
3251+
);
3252+
}
3253+
3254+
expect(
3255+
container.querySelector("#parent-form")?.getAttribute("action")
3256+
).toBe("/parent?index=keep");
3257+
expect(
3258+
container.querySelector("#index-form")?.getAttribute("action")
3259+
).toBe("/parent?index&index=keep");
3260+
3261+
fireEvent.click(screen.getByText("Submit from parent"));
3262+
await tick();
3263+
await waitFor(() => screen.getByText(new RegExp("PARENT ACTION")));
3264+
expect(getHtml(container.querySelector("#parent")!)).toContain(
3265+
"PARENT ACTION: http://localhost/parent?index=keep"
3266+
);
3267+
3268+
fireEvent.click(screen.getByText("Submit from index"));
3269+
await tick();
3270+
await waitFor(() => screen.getByText(new RegExp("INDEX ACTION")));
3271+
expect(getHtml(container.querySelector("#index")!)).toContain(
3272+
"INDEX ACTION: http://localhost/parent?index&index=keep"
3273+
);
3274+
});
3275+
});
3276+
30033277
it("allows user to specify search params and hash", async () => {
30043278
let router = createTestRouter(
30053279
createRoutesFromElements(
@@ -5370,9 +5644,9 @@ function testDomRouter(
53705644
let { container } = render(<RouterProvider router={router} />);
53715645
expect(container.innerHTML).not.toMatch(/my-key/);
53725646
await waitFor(() =>
5373-
// React `useId()` results in either `:r2a:` or `:rp:` depending on
5374-
// `DataBrowserRouter`/`DataHashRouter`
5375-
expect(container.innerHTML).toMatch(/(:r2a:|:rp:),my-key/)
5647+
// React `useId()` results in something such as `:r2a:`, `:r2i:`,
5648+
// `:rt:`, or `:rp:` depending on `DataBrowserRouter`/`DataHashRouter`
5649+
expect(container.innerHTML).toMatch(/(:r[0-9]?[a-z]:),my-key/)
53765650
);
53775651
});
53785652
});

packages/react-router-dom/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,9 +1635,13 @@ export function useFormAction(
16351635
// since it might not apply to our contextual route. We add it back based
16361636
// on match.route.index below
16371637
let params = new URLSearchParams(path.search);
1638-
if (params.has("index") && params.get("index") === "") {
1638+
let indexValues = params.getAll("index");
1639+
let hasNakedIndexParam = indexValues.some((v) => v === "");
1640+
if (hasNakedIndexParam) {
16391641
params.delete("index");
1640-
path.search = params.toString() ? `?${params.toString()}` : "";
1642+
indexValues.filter((v) => v).forEach((v) => params.append("index", v));
1643+
let qs = params.toString();
1644+
path.search = qs ? `?${qs}` : "";
16411645
}
16421646
}
16431647

packages/router/router.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4169,16 +4169,23 @@ function normalizeTo(
41694169
path.hash = location.hash;
41704170
}
41714171

4172-
// Add an ?index param for matched index routes if we don't already have one
4173-
if (
4174-
(to == null || to === "" || to === ".") &&
4175-
activeRouteMatch &&
4176-
activeRouteMatch.route.index &&
4177-
!hasNakedIndexQuery(path.search)
4178-
) {
4179-
path.search = path.search
4180-
? path.search.replace(/^\?/, "?index&")
4181-
: "?index";
4172+
// Account for `?index` params when routing to the current location
4173+
if ((to == null || to === "" || to === ".") && activeRouteMatch) {
4174+
let nakedIndex = hasNakedIndexQuery(path.search);
4175+
if (activeRouteMatch.route.index && !nakedIndex) {
4176+
// Add one when we're targeting an index route
4177+
path.search = path.search
4178+
? path.search.replace(/^\?/, "?index&")
4179+
: "?index";
4180+
} else if (!activeRouteMatch.route.index && nakedIndex) {
4181+
// Remove existing ones when we're not
4182+
let params = new URLSearchParams(path.search);
4183+
let indexValues = params.getAll("index");
4184+
params.delete("index");
4185+
indexValues.filter((v) => v).forEach((v) => params.append("index", v));
4186+
let qs = params.toString();
4187+
path.search = qs ? `?${qs}` : "";
4188+
}
41824189
}
41834190

41844191
// If we're operating within a basename, prepend it to the pathname. If

0 commit comments

Comments
 (0)