Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG maybe] Website - Clicking the components in "Stream HTML straight from the server" writes an entire redirect to the back button cache #2797

Open
MrDwarf7 opened this issue Dec 20, 2024 · 7 comments

Comments

@MrDwarf7
Copy link

Considering the way the feature is worded it seems as though it should function similarly to something like a htm-x swap - I don't believe it should be rendering entire pages.

Interacting with the components for:

Lemonade,
Lemon-honey tea,
Lemondrop Martini

Do re-render the section of the page - but they also write an entry to the back and forward browser cache - meaning it's not swapping in-place but swapping the page.
Not sure if this is intended but would like to report it as it's a component of your website and therefore an on-boarding experience & brand name showcase type of thing.

Feel free to close this if its working as intended - otherwise it could be something that Preact itself is doing too, who knows.

@rschristian
Copy link
Contributor

rschristian commented Dec 21, 2024

I don't believe it should be rendering entire pages.

Do re-render the section of the page - but they also write an entry to the back and forward browser cache - meaning it's not swapping in-place but swapping the page.

That isn't actually the case, no. history.pushState allows you to manipulate the history stack freely without navigating. In fact, this is how most client-side routing works: upon "navigating", JS swaps out the page content (be that JS-generated or Partials) and pushes a new state into the history stack, allowing you to keep using your browser's forward & back buttons to navigate. If you didn't update the history stack, when the user reloads the page, the content will switch back to its initial state which is often undesired.

As such, Partials (and therefore that demo) aren't rerendering the page at all, it simply requests the recipe HTML from the server and swaps it in in-place, updating the history stack (and by extension the URL) accordingly to maintain navigation (bit unnecessary for this demo of course, but it's a good example). This is really easy to see from your browser's devtools, upon switching recipes only the relevant HTML is sent via the network, the rest of the page doesn't need to change.

@MrDwarf7
Copy link
Author

Is it then intentional that back button navigation/mouse bind takes me to a page that consists only of the miniaturized/rendered component?

For instance navigating to this by direct pasting -

https://fresh.deno.dev/recipes/lemon-honey-tea

Will take me to a page that only consists of the new content? This may be the correct case here when navigating directly which means the provided path is working as intended.
Is it then also intentional that after the second link click, IE:

Clicking 'Lemonade' then back simply takes me back to the previous state which is correct.

Refreshing the page to rese.

  • Clicking 'Lemonade'
  • Clicking 'Lemon-honey-tea' then hitting back.
  • This takes me to a page that purely consists of the newly delivered content (Which is only the partial and not the page I was just on, nor the page I was on with the previous partial in it's place)

Hitting the back button will take me to a fullscreen page where the content of the page is only the below HTML:

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h2 class="mb-2 font-extrabold">
            Lemonade</h2>
        <ul>
            <li>
                1 ¾ cups white sugar</li>
            <li>
                1 cup water</li>
            <li>
                9 lemons</li>
            <li>
                7 cups ice water</li>
            <li>
                Ice</li>
        </ul>
        <script type="module" nonce="dd873b60a0c2463eb7b2d5f3905e4496">
        import { boot } from "/_fresh/js/8d8ec4eee3cadad8cd50fa7fe3b8757ad3756947/fresh-runtime.js";boot({},"[[]]");</script>
    </body>
</html>

This seems like odd behavior - at least from a first look/product showcase to take someone away from the website & branding.
If this is actually intentional then my apologies, just seemed like unintentional behavior from something in state tracking etc.

@rschristian
Copy link
Contributor

rschristian commented Dec 21, 2024

Is it then intentional that back button navigation/mouse bind takes me to a page that consists only of the miniaturized/rendered component?

That bit does seem like a bug, yes. Blind guess, but might not be calling .preventDefault on the 'popstate' event. Whoops, late-night brain. You can't prevent default on popstate

@MrDwarf7
Copy link
Author

Okay so I realized the actual code for the website is in this repo and I'm having a look through it -
A noteworthy point after some further experimenting -

Clicking a single link, then back, then the next one in the element will not produce the bug.
However clicking a link, then another, then hitting back will.

I will have a look around here:

addEventListener("popstate", async (e) => {

(This was what came up with a quick repo search for popstate as you've mentioned)

With the above behavior it seems to me we're only storing a single state change, calling the routing (and therefore only rendering the HTML) for the partial that was last created, when User is navigating back.
Does this seem possible as to what's happening here?

@rschristian
Copy link
Contributor

First off, full disclosure: I work on Preact stuff and pop in here every now and then to see if Preact bugs are reported, maybe to help out when I can. I've never actually loaded up Fresh before (sorry Marvin!).

Clicking a single link, then back, then the next one in the element will not produce the bug.

Unfortunately I think it does. It appears to me to be doing a full back nav to the page (though this isn't as problematic as you're going from https://fresh.deno.dev/<partial> to https://fresh.deno.dev/, which still has a full page of content). Granted I'm not quite sure how a "default Partial" (if you will) works there, but I'm not sure that's working correctly.

Would have to look into it more (doing this from my phone, sorry!), but generally the history API should be storing all state itself, if I'm understanding you correctly. There might be an over zealous .replaceState(...) somewhere, removing past state entries (causing popstate to do a full back nav as it has hit the bottom of the stack).

Really good spot on this issue though.

@MrDwarf7
Copy link
Author

Ahh true, I'd not have guessed that you hadn't worked with Fresh directly haha.

With Fresh seeming to be a fairly 'close-to' building block on top of Preact, I'm not surprised your blind guesses were right on point.
Came back to see your comment and glad to say we've come to similar conclusions:

function checkClientNavEnabled(el: HTMLElement) {
const setting = el.closest(`[${CLIENT_NAV_ATTR}]`);
if (setting === null) return false;
return setting.getAttribute(CLIENT_NAV_ATTR) !== "false";
}
// Keep track of history state to apply forward or backward animations
let index = history.state?.index || 0;
if (!history.state) {
const state: FreshHistoryState = {
index,
scrollX,
scrollY,
};
history.replaceState(state, document.title);
}
function maybeUpdateHistory(nextUrl: URL) {
// Only add history entry when URL is new. Still apply
// the partials because sometimes users click a link to
// "refresh" the current page.
if (nextUrl.href !== globalThis.location.href) {
const state: FreshHistoryState = {
index,
scrollX: globalThis.scrollX,
scrollY: globalThis.scrollY,
};
// Store current scroll position
history.replaceState({ ...state }, "", location.href);
// Now store the new position
index++;
state.scrollX = 0;
state.scrollY = 0;
history.pushState(state, "", nextUrl.href);
}
}

addEventListener("popstate", async (e) => {
// When state is `null` then the browser navigated to a document
// fragment. In this case we do nothing.
if (e.state === null) {
// Reset to browser default
if (history.scrollRestoration) {
history.scrollRestoration = "auto";
}
return;
}
const state: FreshHistoryState = history.state;
const nextIdx = state.index ?? index + 1;
index = nextIdx;
const setting = document.querySelector(`[${CLIENT_NAV_ATTR}]`);
if (setting === null || setting.getAttribute(CLIENT_NAV_ATTR) === "false") {
location.reload();
return;
}
// We need to keep track of that ourselves since we do client side
// navigation.
if (history.scrollRestoration) {
history.scrollRestoration = "manual";
}
const url = new URL(location.href, location.origin);
try {
await fetchPartials(url, url);
updateLinks(url);
scrollTo({
left: state.scrollX ?? 0,
top: state.scrollY ?? 0,
behavior: "instant",
});
} catch (err) {
// If the response didn't contain a partial, then we can only
// do a reload.
if (err instanceof NoPartialsError) {
location.reload();
return;
}
throw err;
}
});

The snippet above had me curious with the direct assignment of a/the state change - and your mention of a .replaceState(...) seems to align pretty well here - There's calls to both replace & push being handled, and the addEventListener("popstate", async (e) => {}); call happening close by.

I've been able to re-produce this locally while running in dev (from root - deno task www) and will continue to have a fiddle around with it but will likely want some guidance on how exactly the Dev's want to handle it or what they'd want to implement to get this working as intended, especially so being that the code I'm looking at here is in src/.

@rschristian
Copy link
Contributor

With Fresh seeming to be a fairly 'close-to' building block on top of Preact, I'm not surprised your blind guesses were right on point.

Well none of this is actually related to Preact (or at least doesn't seem to be at the moment), just normal web API stuff. I've worked on some client-side routing stuff in the past though which is the source from my guesses.

Will have to have a fiddle w/ it myself though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants