Skip to content

Commit fb70fed

Browse files
Do not include hash in useFormAction() for unspecified actions (#10758)
Co-authored-by: Jacob Ebey <[email protected]>
1 parent 46806a4 commit fb70fed

File tree

3 files changed

+47
-26
lines changed

3 files changed

+47
-26
lines changed

.changeset/form-action-hash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Do not include hash in `useFormAction()` for unspecified actions since it cannot be determined on the server and causes hydration issues

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

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2529,7 +2529,7 @@ function testDomRouter(
25292529
}
25302530

25312531
describe("static routes", () => {
2532-
it("includes search params + hash when no action is specified", async () => {
2532+
it("includes search params when no action is specified", async () => {
25332533
let router = createTestRouter(
25342534
createRoutesFromElements(
25352535
<Route path="/">
@@ -2545,11 +2545,11 @@ function testDomRouter(
25452545
let { container } = render(<RouterProvider router={router} />);
25462546

25472547
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2548-
"/foo/bar?a=1#hash"
2548+
"/foo/bar?a=1"
25492549
);
25502550
});
25512551

2552-
it("does not include search params + hash when action='.'", async () => {
2552+
it("does not include search params when action='.'", async () => {
25532553
let router = createTestRouter(
25542554
createRoutesFromElements(
25552555
<Route path="/">
@@ -2567,7 +2567,7 @@ function testDomRouter(
25672567
);
25682568
});
25692569

2570-
it("does not include search params + hash when action=''", async () => {
2570+
it("does not include search params when action=''", async () => {
25712571
let router = createTestRouter(
25722572
createRoutesFromElements(
25732573
<Route path="/">
@@ -2587,7 +2587,7 @@ function testDomRouter(
25872587
});
25882588

25892589
describe("layout routes", () => {
2590-
it("includes search params + hash when no action is specified", async () => {
2590+
it("includes search params when no action is specified", async () => {
25912591
let router = createTestRouter(
25922592
createRoutesFromElements(
25932593
<Route path="/">
@@ -2605,11 +2605,11 @@ function testDomRouter(
26052605
let { container } = render(<RouterProvider router={router} />);
26062606

26072607
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2608-
"/foo/bar?a=1#hash"
2608+
"/foo/bar?a=1"
26092609
);
26102610
});
26112611

2612-
it("does not include search params + hash when action='.'", async () => {
2612+
it("does not include search params when action='.'", async () => {
26132613
let router = createTestRouter(
26142614
createRoutesFromElements(
26152615
<Route path="/">
@@ -2631,7 +2631,7 @@ function testDomRouter(
26312631
);
26322632
});
26332633

2634-
it("does not include search params + hash when action=''", async () => {
2634+
it("does not include search params when action=''", async () => {
26352635
let router = createTestRouter(
26362636
createRoutesFromElements(
26372637
<Route path="/">
@@ -2655,7 +2655,7 @@ function testDomRouter(
26552655
});
26562656

26572657
describe("index routes", () => {
2658-
it("includes search params + hash when no action is specified", async () => {
2658+
it("includes search params when no action is specified", async () => {
26592659
let router = createTestRouter(
26602660
createRoutesFromElements(
26612661
<Route path="/">
@@ -2673,11 +2673,11 @@ function testDomRouter(
26732673
let { container } = render(<RouterProvider router={router} />);
26742674

26752675
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2676-
"/foo/bar?index&a=1#hash"
2676+
"/foo/bar?index&a=1"
26772677
);
26782678
});
26792679

2680-
it("does not include search params + hash action='.'", async () => {
2680+
it("does not include search params action='.'", async () => {
26812681
let router = createTestRouter(
26822682
createRoutesFromElements(
26832683
<Route path="/">
@@ -2699,7 +2699,7 @@ function testDomRouter(
26992699
);
27002700
});
27012701

2702-
it("does not include search params + hash action=''", async () => {
2702+
it("does not include search params action=''", async () => {
27032703
let router = createTestRouter(
27042704
createRoutesFromElements(
27052705
<Route path="/">
@@ -2777,7 +2777,7 @@ function testDomRouter(
27772777
let { container } = render(<RouterProvider router={router} />);
27782778

27792779
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2780-
"/foo/bar?index&a=1#hash"
2780+
"/foo/bar?index&a=1"
27812781
);
27822782
});
27832783

@@ -2816,7 +2816,7 @@ function testDomRouter(
28162816
});
28172817

28182818
describe("dynamic routes", () => {
2819-
it("includes search params + hash when no action is specified", async () => {
2819+
it("includes search params when no action is specified", async () => {
28202820
let router = createTestRouter(
28212821
createRoutesFromElements(
28222822
<Route path="/">
@@ -2832,11 +2832,11 @@ function testDomRouter(
28322832
let { container } = render(<RouterProvider router={router} />);
28332833

28342834
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2835-
"/foo/bar?a=1#hash"
2835+
"/foo/bar?a=1"
28362836
);
28372837
});
28382838

2839-
it("does not include search params + hash action='.'", async () => {
2839+
it("does not include search params action='.'", async () => {
28402840
let router = createTestRouter(
28412841
createRoutesFromElements(
28422842
<Route path="/">
@@ -2856,7 +2856,7 @@ function testDomRouter(
28562856
);
28572857
});
28582858

2859-
it("does not include search params + hash when action=''", async () => {
2859+
it("does not include search params when action=''", async () => {
28602860
let router = createTestRouter(
28612861
createRoutesFromElements(
28622862
<Route path="/">
@@ -2878,7 +2878,7 @@ function testDomRouter(
28782878
});
28792879

28802880
describe("splat routes", () => {
2881-
it("includes search params + hash when no action is specified", async () => {
2881+
it("includes search params when no action is specified", async () => {
28822882
let router = createTestRouter(
28832883
createRoutesFromElements(
28842884
<Route path="/">
@@ -2894,11 +2894,11 @@ function testDomRouter(
28942894
let { container } = render(<RouterProvider router={router} />);
28952895

28962896
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2897-
"/foo?a=1#hash"
2897+
"/foo?a=1"
28982898
);
28992899
});
29002900

2901-
it("does not include search params + hash when action='.'", async () => {
2901+
it("does not include search params when action='.'", async () => {
29022902
let router = createTestRouter(
29032903
createRoutesFromElements(
29042904
<Route path="/">
@@ -2918,7 +2918,7 @@ function testDomRouter(
29182918
);
29192919
});
29202920

2921-
it("does not include search params + hash when action=''", async () => {
2921+
it("does not include search params when action=''", async () => {
29222922
let router = createTestRouter(
29232923
createRoutesFromElements(
29242924
<Route path="/">
@@ -2938,6 +2938,24 @@ function testDomRouter(
29382938
);
29392939
});
29402940
});
2941+
2942+
it("allows user to specify search params and hash", async () => {
2943+
let router = createTestRouter(
2944+
createRoutesFromElements(
2945+
<Route path="/">
2946+
<Route path="foo">
2947+
<Route path="bar" element={<Form action=".?a=1#newhash" />} />
2948+
</Route>
2949+
</Route>
2950+
),
2951+
{ window: getWindow("/foo/bar?a=1#hash") }
2952+
);
2953+
let { container } = render(<RouterProvider router={router} />);
2954+
2955+
expect(container.querySelector("form")?.getAttribute("action")).toBe(
2956+
"/foo/bar?a=1#newhash"
2957+
);
2958+
});
29412959
});
29422960

29432961
describe('<Form action relative="path">', () => {

packages/react-router-dom/index.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,17 +1157,15 @@ export function useFormAction(
11571157
let path = { ...useResolvedPath(action ? action : ".", { relative }) };
11581158

11591159
// Previously we set the default action to ".". The problem with this is that
1160-
// `useResolvedPath(".")` excludes search params and the hash of the resolved
1161-
// URL. This is the intended behavior of when "." is specifically provided as
1160+
// `useResolvedPath(".")` excludes search params of the resolved URL. This is
1161+
// the intended behavior of when "." is specifically provided as
11621162
// the form action, but inconsistent w/ browsers when the action is omitted.
11631163
// https://github.com/remix-run/remix/issues/927
11641164
let location = useLocation();
11651165
if (action == null) {
1166-
// Safe to write to these directly here since if action was undefined, we
1166+
// Safe to write to this directly here since if action was undefined, we
11671167
// would have called useResolvedPath(".") which will never include a search
1168-
// or hash
11691168
path.search = location.search;
1170-
path.hash = location.hash;
11711169

11721170
// When grabbing search params from the URL, remove the automatically
11731171
// inserted ?index param so we match the useResolvedPath search behavior

0 commit comments

Comments
 (0)