Skip to content

Commit db7eb57

Browse files
authored
Fix manifest version mismatch reload losing query parameters and hash (#14813)
1 parent 639eb93 commit db7eb57

File tree

4 files changed

+90
-1
lines changed

4 files changed

+90
-1
lines changed

.changeset/warm-clouds-shine.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Preserve query parameters and hash on manifest version mismatch reload

integration/fog-of-war-test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,82 @@ test.describe("Fog of War", () => {
14551455
expect(wrongManifestRequests).toEqual([]);
14561456
});
14571457

1458+
test("manifest version mismatch reload should preserve query parameters and hash", async ({
1459+
page,
1460+
}) => {
1461+
let fixture = await createFixture({
1462+
files: {
1463+
"app/routes/_index.tsx": js`
1464+
import { Link, useLocation } from "react-router";
1465+
1466+
export default function Index() {
1467+
const location = useLocation();
1468+
return (
1469+
<div>
1470+
<h1>Home</h1>
1471+
<p data-location>Location: {location.pathname + location.search + location.hash}</p>
1472+
<Link to="/other?token=abc123&ref=campaign#section1">Go to Other</Link>
1473+
</div>
1474+
);
1475+
}
1476+
`,
1477+
"app/routes/other.tsx": js`
1478+
import { useLocation } from "react-router";
1479+
1480+
export default function Other() {
1481+
const location = useLocation();
1482+
return (
1483+
<div>
1484+
<h1>Other Page</h1>
1485+
<p data-location2>Location: {location.pathname + location.search + location.hash}</p>
1486+
</div>
1487+
);
1488+
}
1489+
`,
1490+
},
1491+
});
1492+
1493+
// Trigger mismatch + hard reload when trying to patch the /other route
1494+
await page.route(/\/__manifest/, async (route) => {
1495+
if (route.request().url().includes(encodeURIComponent("/other"))) {
1496+
await route.fulfill({
1497+
status: 204,
1498+
headers: {
1499+
"X-Remix-Reload-Document": "true",
1500+
},
1501+
});
1502+
} else {
1503+
await route.continue();
1504+
}
1505+
});
1506+
1507+
let appFixture = await createAppFixture(fixture);
1508+
let app = new PlaywrightFixture(appFixture, page);
1509+
1510+
// Start on home page
1511+
await app.goto("/");
1512+
await page.waitForSelector("h1");
1513+
await expect(page.locator("[data-location]")).toHaveText("Location: /");
1514+
1515+
// Click link to /other with query params and hash
1516+
// This should trigger manifest fetch -> version mismatch -> hard reload
1517+
await app.clickLink("/other?token=abc123&ref=campaign#section1");
1518+
1519+
// Wait for the page to reload and render
1520+
await page.waitForSelector("[data-location2]", { timeout: 5000 });
1521+
1522+
// Query parameters and hash should be preserved after reload
1523+
await expect(page.locator("[data-location2]")).toHaveText(
1524+
"Location: /other?token=abc123&ref=campaign#section1",
1525+
);
1526+
1527+
// Also verify the URL in the browser
1528+
const currentUrl = page.url();
1529+
expect(currentUrl).toContain("token=abc123");
1530+
expect(currentUrl).toContain("ref=campaign");
1531+
expect(currentUrl).toContain("#section1");
1532+
});
1533+
14581534
test.describe("routeDiscovery=initial", () => {
14591535
test("loads full manifest on initial load", async ({ page }) => {
14601536
let fixture = await createFixture({

packages/react-router/lib/dom-export/hydrated-router.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ function createHydratedRouter({
199199
ssrInfo.context.future.unstable_trailingSlashAwareDataRequests,
200200
),
201201
patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(
202+
() => router,
202203
ssrInfo.manifest,
203204
ssrInfo.routeModules,
204205
ssrInfo.context.ssr,

packages/react-router/lib/dom/ssr/fog-of-war.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { RouteModules } from "./routeModules";
88
import type { EntryRoute } from "./routes";
99
import { createClientRoutes } from "./routes";
1010
import type { ServerBuild } from "../../server-runtime/build";
11+
import { createPath } from "../../router/history";
1112

1213
// Currently rendered links that may need prefetching
1314
const nextPaths = new Set<string>();
@@ -67,6 +68,7 @@ export function getPartialManifest(
6768
}
6869

6970
export function getPatchRoutesOnNavigationFunction(
71+
getRouter: () => DataRouter,
7072
manifest: AssetsManifest,
7173
routeModules: RouteModules,
7274
ssr: boolean,
@@ -82,9 +84,14 @@ export function getPatchRoutesOnNavigationFunction(
8284
if (discoveredPaths.has(path)) {
8385
return;
8486
}
87+
let { state } = getRouter();
8588
await fetchAndApplyManifestPatches(
8689
[path],
87-
fetcherKey ? window.location.href : path,
90+
// If we're patching for a fetcher call, reload the current location
91+
// Otherwise prefer any ongoing navigation location
92+
fetcherKey
93+
? window.location.href
94+
: createPath(state.navigation.location || state.location),
8895
manifest,
8996
routeModules,
9097
ssr,

0 commit comments

Comments
 (0)