Skip to content

Commit d072dc2

Browse files
authored
fix: avoid reload for user-code history changes (#3045) (#3048)
The Fresh partials popstate handler causes page reloads when user code makes use of navigation history state. This can lead to a loss of user data in a browsing context. To fix this behavior, FreshHistoryState now has a flag which validates partials functionality/explicitly opts-in to any partial related location management.
1 parent 3e211dc commit d072dc2

File tree

2 files changed

+41
-0
lines changed

2 files changed

+41
-0
lines changed

src/runtime/client/partials.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export const PARTIAL_ATTR = "f-partial";
2727

2828
class NoPartialsError extends Error {}
2929

30+
// Fresh partials history updates set the fClientNav flag
31+
// and prevent reloads in the popstate handler when
32+
// user-code triggers history navigation events.
3033
export interface FreshHistoryState {
34+
fClientNav: boolean;
3135
index: number;
3236
scrollX: number;
3337
scrollY: number;
@@ -43,6 +47,7 @@ function checkClientNavEnabled(el: HTMLElement) {
4347
let index = history.state?.index || 0;
4448
if (!history.state) {
4549
const state: FreshHistoryState = {
50+
fClientNav: true,
4651
index,
4752
scrollX,
4853
scrollY,
@@ -56,6 +61,7 @@ function maybeUpdateHistory(nextUrl: URL) {
5661
// "refresh" the current page.
5762
if (nextUrl.href !== globalThis.location.href) {
5863
const state: FreshHistoryState = {
64+
fClientNav: true,
5965
index,
6066
scrollX: globalThis.scrollX,
6167
scrollY: globalThis.scrollY,
@@ -164,6 +170,9 @@ addEventListener("popstate", async (e) => {
164170
}
165171
return;
166172
}
173+
// Do nothing if Fresh navigation is not explicitly opted-in.
174+
// Other applications might manage scrollRestoration individually.
175+
if (!e.state.fClientNav) return;
167176

168177
const state: FreshHistoryState = history.state;
169178
const nextIdx = state.index ?? index + 1;

tests/partials_test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2712,3 +2712,35 @@ Deno.test({
27122712
});
27132713
},
27142714
});
2715+
2716+
Deno.test({
2717+
name: "partials - independent user popstate",
2718+
fn: async () => {
2719+
const app = testApp()
2720+
.get("/", (ctx) => {
2721+
return ctx.render(
2722+
<Doc>
2723+
<div class="container">
2724+
</div>
2725+
</Doc>,
2726+
);
2727+
});
2728+
2729+
await withBrowserApp(app, async (page, address) => {
2730+
await page.goto(address, { waitUntil: "load" });
2731+
await page.locator<HTMLDivElement>(".container").evaluate((el) => {
2732+
const dynamicContent = document.createElement("span");
2733+
dynamicContent.classList.add("dynamic-content");
2734+
el.appendChild(dynamicContent);
2735+
});
2736+
await page.evaluate(() => {
2737+
window.history.replaceState({ custom: true }, "", "#");
2738+
window.history.pushState({ custom: true }, "", "#custom");
2739+
});
2740+
// Fresh partials popstate gets called on back navigation and
2741+
// should exit early/avoid reload due to custom user history entries
2742+
await page.evaluate(() => window.history.go(-1));
2743+
await page.locator(".dynamic-content").wait();
2744+
});
2745+
},
2746+
});

0 commit comments

Comments
 (0)