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

What happens when returning to entries captured by app-history #99

Open
jakearchibald opened this issue Apr 21, 2021 · 19 comments
Open

Comments

@jakearchibald
Copy link

jakearchibald commented Apr 21, 2021

This came up in whatwg/html#6207 and it might be worth 'fixing' in the new API.

  1. Page is /.
  2. User clicks link, which navigates to /article-1, which is handled within the current document by responding to the "navigate" event.
  3. User presses refresh, or navigates away and presses back (assuming no bfcache).
  4. User presses back.

Currently, the equivalent pushState browser behaviour is:

  1. /article-1 fetched and displayed.
  2. Switch url and state to / (no fetch).

I wonder how obvious it is to developers that they need to cater for this. It seems possible that the resources at:

  • / is a full app, capable of transitions etc etc
  • /article-1 is just the article. HTML & CSS.

…meaning that /article-1 is unprepared for an internal state change to /.

Other ways this could be handled:

  1. /article-1 fetched and displayed.
  2. / fetched and displayed.

This means entries that were previously using the same document are now using different documents, which is weird in its own way. Although, this is what should happen in a literal interpretation of the spec.

Or:

  1. / fetched and displayed, but with /article-1 state.
  2. Switch url and state to / (no fetch).

This means fetching something other than the URL that's in the URL bar, but it loads the same code that previously handled the transition from / to /article-1.

@tbondwilkinson
Copy link
Contributor

This is a really hard one, because I think it would be confusing to default to loading as if we were back in '/' when the URL bar actually says '/article-1' but you're right that this is a place where developers have to add special handling (basically they have to redo in JS all the things that the page formerly did).

I don't think we should go the route of making formerly same-document history entries cross-document, that also feels bad.

To me this problem is more about how fast we can display HTML to the user on a back navigation. Right now, it's a waste because when you're on a page with a URL, but you've done a history.pushState, you know that this is the HTML you'd want to represent as the page. But when a back navigation occurs, you're going to have to re-construct this entire page, because this HTML is NOT stored in any cache, unlike a fresh page load.

So if I were to go about fixing this problem, I would think about ways that the browser could snapshot the contents of the DOM when the URL bar changes in a same-document way, as if that DOM had been requested explicitly from the network. Then, even though this isn't a full BFCache reload (since the JS context is recreated), you would at LEAST not lose the DOM content, which I assume is cheaper to cache than full JS context.

Keep in mind too that developers always have to handle this case, because at any moment the user could reload. Interestingly, reloading does transform entries into cross-document, so e.g. if you had / and /article-1 as entries and refreshed on /article-1, / is now in a different document from the one you just loaded. This has always been fairly confusing to me - and I wish that a reload had different behavior.

@domenic
Copy link
Collaborator

domenic commented Apr 21, 2021

In general app history builds on the existing session history model, and I don't think we have any particular opportunities to change it. So I think the behavior here should be the same as it is for pushState. I don't know how we'd spec something else.

I wonder how obvious it is to developers that they need to cater for this. It seems possible that the resources at:

My understanding is that typically SPAs are done by either:

  • Making every URL respond with the same shell, which then fills in the page-specific data with a fetch call
  • Making every URL respond with the same shell but with its page-specific data pre-filled in by URL-dependent server rendering.

So I don't think it's very typical that / is treated specially and /article-1 is not prepared to display an app. After all, what would happen if the user copied and pasted the /article-1 URL to their friends?

@tbondwilkinson
Copy link
Contributor

So I don't think it's very typical that / is treated specially and /article-1 is not prepared to display an app. After all, what would happen if the user copied and pasted the /article-1 URL to their friends?

Well, I do think you're over-estimating apps.

But further I think what's more frustrating than having to handle this specially is that to handle this, you have to either keep server-side rendering in sync with client-side rendering, which isn't trivial OR you have to do all client-side rendering, which has time-to-interaction implications. Vs. with multi-page applications, browsers do a really good job of making reloads and navigations cheap, with SPA, reloads and navigations can be very expensive if JS context is lost.

@jakearchibald
Copy link
Author

After all, what would happen if the user copied and pasted the /article-1 URL to their friends?

That bit is fine. The server sends HTML. It's when that document also needs to handle /, that's the bit which might be problematic.

As you say, if all URLs are rewriting to the same app JS, it's fine. It's possible that's what everyone does.

@jakearchibald
Copy link
Author

@tbondwilkinson

Interestingly, reloading does transform entries into cross-document, so e.g. if you had / and /article-1 as entries and refreshed on /article-1, / is now in a different document from the one you just loaded.

I don't think so, not in Chrome anyway. Where are you seeing this?

@jakearchibald
Copy link
Author

Yeah, I think my mental model of how pushState was supposed to work is broken. I thought that, as long as the pushed URL gave you access to the core content addressed by the URL, then you're good.

Interestingly, if I Google for 'pushState demo', the first result with a working demo is https://css-tricks.com/using-the-html5-history-api/, and the demo makes the same incorrect assumption.

I have a vague worry that the current API encourages this thinking. Eg:

// The amazing lightbox library!
appHistory.addEventListener("navigate", event => {
  if (!event.canRespond || event.hashChange) return;
  const url = new URL(event.destination.url);
  if (!/\.(jpe?g|gif|png|avif|webp)$/.test(url.pathname)) return;

  displayImageInAModal(url);
});

This captures navigations to images and displays them in-page. The link is sharable (it'll just point to the image). The back button will appear to work. It'll even sometimes work across documents thanks to bfcache.

I guess we just have to document that the above is not okay.

@tbondwilkinson
Copy link
Contributor

I don't think so, not in Chrome anyway. Where are you seeing this?

For instance starting on google.com, call pushState(undefined, undefined, '/maps') Refresh the page, you'll end up at the google maps frontend, not google search homepage with a '/maps' path.

Or am I misunderstanding what you mean?

@tbondwilkinson
Copy link
Contributor

The image example is interesting - I assume open in new tab/window would still work as expected. But perhaps we'd only allow making cross-document navigations same-document if the other document is of the same "type", e.g a webpage and not a resource.

@jakearchibald
Copy link
Author

Interestingly, reloading does transform entries into cross-document, so e.g. if you had / and /article-1 as entries and refreshed on /article-1, / is now in a different document from the one you just loaded.

Maybe I'm reading this wrong. Here's a test:

  1. Go to https://example.com.
  2. In the console, run history.pushState({}, '', '/article-1').
  3. Press refresh.
  4. Press back.

When you said "/ is now in a different document from the one you just loaded", I thought you meant that 4 would result in a change of document.

3 results in a change of document, but it updates both history entries, so 4 traverses to the same document.

@tbondwilkinson
Copy link
Contributor

tbondwilkinson commented Apr 22, 2021 via email

@jakearchibald
Copy link
Author

I'm not seeing that behaviour in Chrome, Firefox, or Safari https://static-misc-2.glitch.me/push-state-test/

@tbondwilkinson
Copy link
Contributor

Ah, now I see what you mean. You're right! :) Another fun quirk.

@jakearchibald
Copy link
Author

It's consistent with navigating away then pressing back (unless bfcache), and hash-change navigations, so that's one thing at least!

@bathos
Copy link

bathos commented Apr 22, 2021

Although it doesn’t seem like this is something that can be solved (where it actually needs solving, I mean?) by AppHistory alone, perhaps if AppHistory ends up being friendly to pairing with the proposed URLPattern and/or “Declarative routing” APIs, this would in turn make it more straightforward and attractive to share a single source of truth for route definitions between the client, service worker, and backend. If that pattern were easier to realize, it might help reduce the odds of folks accidentally introducing disagreements between server and client routing.

@jakearchibald
Copy link
Author

@domenic I'm happy for this to close unless you're interested in solving the image use-case in #99 (comment), perhaps in some 'opt-in' way that forces the new history entry to use its own document state. Fwiw, my PR will probably support that.

Otherwise, feels like we should stick with the current behaviour.

@domenic
Copy link
Collaborator

domenic commented Apr 23, 2021

I think we should probably stick with the current behavior, but I'm happy to keep this open to track efforts about documentation, or thinking if there's some way to steer people away from such behavior, or the ideas that @bathos mentions.

@domenic
Copy link
Collaborator

domenic commented May 27, 2021

I'm trying to refresh myself on this to see if we can add documentation but I unfortunately lost track of the problem. Given the lightbox image code in #99 (comment) what is the problem scenario? Is the problem that the navigate handler isn't properly handling navigations to other URLs and closing the modal in that case, so e.g. the back button never closes the modal?

@jakearchibald
Copy link
Author

  1. User is on /cool-app
  2. User navigates to /cool-app/photo.webp
  3. This navigation is captured, and a lightbox is shown (we now have two history entries pointing to the same document)
  4. User presses refresh, and the document in both history entries is updated to the image at /cool-app/photo.webp (not in a lightbox)
  5. User presses back, URL changes to /cool-app, but nothing else happens, they're still looking at the image.

@jakearchibald
Copy link
Author

One solution might be an option that, in step 3, creates a new history entry with the same document, but a different document state.

This means the reload in step 4 would only replace the document in the second history entry.

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

4 participants