Skip to content

Commit 1f294a3

Browse files
authored
Fix up same-hash link short circuiting logic (#10408)
1 parent 0358dd8 commit 1f294a3

File tree

3 files changed

+56
-5
lines changed

3 files changed

+56
-5
lines changed

.changeset/same-hash-links.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+
"Same hash" navigations no longer re-run loaders to match browser behavior (i.e. `/path#hash -> /path#hash`)

packages/router/__tests__/router-test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,14 +1476,46 @@ describe("a router", () => {
14761476
});
14771477
});
14781478

1479-
it("does not load anything on hash change only <Link> navigations", async () => {
1479+
it("does not run loaders on hash change only navigations", async () => {
14801480
let t = initializeTmTest();
14811481
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });
14821482
let A = await t.navigate("/#bar");
14831483
expect(A.loaders.root.stub.mock.calls.length).toBe(0);
14841484
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });
14851485
});
14861486

1487+
it("does not run loaders on same-hash navigations", async () => {
1488+
let t = initializeTmTest({ url: "/#bar" });
1489+
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });
1490+
let A = await t.navigate("/#bar");
1491+
expect(A.loaders.root.stub.mock.calls.length).toBe(0);
1492+
expect(A.loaders.index.stub.mock.calls.length).toBe(0);
1493+
});
1494+
1495+
it("runs loaders on same-hash navigations to new paths", async () => {
1496+
let t = initializeTmTest({ url: "/#bar" });
1497+
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });
1498+
let A = await t.navigate("/foo#bar");
1499+
expect(A.loaders.root.stub.mock.calls.length).toBe(0);
1500+
expect(A.loaders.foo.stub.mock.calls.length).toBe(1);
1501+
});
1502+
1503+
it("runs loaders on hash removal navigations (same path)", async () => {
1504+
let t = initializeTmTest({ url: "/#bar" });
1505+
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });
1506+
let A = await t.navigate("/");
1507+
expect(A.loaders.root.stub.mock.calls.length).toBe(1);
1508+
expect(A.loaders.index.stub.mock.calls.length).toBe(1);
1509+
});
1510+
1511+
it("runs loaders on hash removal navigations (nested path)", async () => {
1512+
let t = initializeTmTest({ url: "/#bar" });
1513+
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });
1514+
let A = await t.navigate("/foo");
1515+
expect(A.loaders.root.stub.mock.calls.length).toBe(0);
1516+
expect(A.loaders.foo.stub.mock.calls.length).toBe(1);
1517+
});
1518+
14871519
it('does not load anything on hash change only empty <Form method="get"> navigations', async () => {
14881520
let t = initializeTmTest();
14891521
expect(t.router.state.loaderData).toMatchObject({ root: "ROOT" });

packages/router/router.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3266,7 +3266,8 @@ function getMatchesToLoad(
32663266
// Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
32673267
isRevalidationRequired ||
32683268
// Clicked the same link, resubmitted a GET form
3269-
currentUrl.toString() === nextUrl.toString() ||
3269+
currentUrl.pathname + currentUrl.search ===
3270+
nextUrl.pathname + nextUrl.search ||
32703271
// Search params affect all loaders
32713272
currentUrl.search !== nextUrl.search ||
32723273
isNewRouteInstance(currentRouteMatch, nextRouteMatch),
@@ -4003,9 +4004,22 @@ function stripHashFromPath(path: To) {
40034004
}
40044005

40054006
function isHashChangeOnly(a: Location, b: Location): boolean {
4006-
return (
4007-
a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash
4008-
);
4007+
if (a.pathname !== b.pathname || a.search !== b.search) {
4008+
return false;
4009+
}
4010+
4011+
if (a.hash === "") {
4012+
// No hash -> hash
4013+
return b.hash !== "";
4014+
} else if (a.hash === b.hash) {
4015+
// current hash -> same hash
4016+
return true;
4017+
} else if (b.hash !== "") {
4018+
// current hash -> new hash
4019+
return true;
4020+
}
4021+
4022+
return false;
40094023
}
40104024

40114025
function isDeferredResult(result: DataResult): result is DeferredResult {

0 commit comments

Comments
 (0)