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

New history event proposal #5562

Closed
frehner opened this issue May 18, 2020 · 10 comments
Closed

New history event proposal #5562

frehner opened this issue May 18, 2020 · 10 comments
Labels
addition/proposal New features or enhancements needs concrete proposal Moving the issue forward requires someone to figure out a detailed plan needs implementer interest Moving the issue forward requires implementers to express interest topic: history

Comments

@frehner
Copy link

frehner commented May 18, 2020

See this exact same proposal but in a different place: WICG/proposals#6

Proposal

Add an event called statechange or historychange that will fire on any change to the history stack, whether that be through the browser's back button, or window.history.pushState or other methods.

This proposed event would be similar to popstate, except that it would fire on all route changes regardless of the source, much like hashchange fires on all hash changes regardless of the source.

Current Problems

hashchange events allowed javascript router libraries (e.g. React Router, vue-router) to easily respond to any routing event when the application is using hash routing.

However, with the HTML5 History API, there is no equivalent event that javascript routers can listen to.

This means that routers have the following limitations/problems:

  • They assume that they're the only Router that exists on the page - Routers require all code to call into it whenever making a URL change
  • Users cannot call window.history.pushState directly and must only use the Router's custom methods
  • Third party libraries that may want to change the URL or cause a Router to update have a difficult time since they can't call native APIs

References:

remix-run/react-router#6304

Example

window.addEventListener('historychange', (event) => {
  console.log('changed')
})

history.pushState(null, null, '/path')  // logs "changed"
location.hash = "sub" // logs "changed"

Side notes

I hope I did this in the correct repo. Let me know if I need to change/update anything. Thanks.

This was created in collaboration with @joeldenning

@annevk annevk added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: history needs concrete proposal Moving the issue forward requires someone to figure out a detailed plan labels May 19, 2020
@frehner
Copy link
Author

frehner commented May 27, 2020

Looking at both popstate and hashchange event interfaces for inspiration, I was thinking that it may be useful to have the statechange event interface look something like the following:

interface StateChangeEvent : Event {
  readonly attribute USVString oldURL;
  readonly attribute USVString newURL;
  readonly attribute any state;
};

dictionary StateChangeEventInit : EventInit {
  USVString oldURL = "";
  USVString newURL = "";
  any state = null;
};

I'm not entirely sure the oldURL would be necessary, but I think making it more in-line with how the hashchange event interface is would potentially be nice.

@frehner
Copy link
Author

frehner commented Jun 4, 2020

I'm not entirely sure the oldURL would be necessary, but I think making it more in-line with how the hashchange event interface is would potentially be nice.

actually, now that I've looked at it, I think it would be great to have the oldURL as well. This would enable people to listen to a single event and only respond to events that they care about - e.g. path changes, and/or hash changes, and/or search changes.

There appears to be lots of people that want to ignore hash changes in the popstate event:

For example: user is on /path, clicks on a link <a href="#sub">link</a>, and then clicks the browser back button to go back to /path. This fires a popstate event but there could be a usecase where the dev doesn't care about hash changes and can bail early - for example, in React, you could bail out of rerendering here.

@joeldenning
Copy link

A couple questions I'm thinking through with this:

  1. Should statechange be fired when you call history.replaceState(history.state, document.title, location.pathname), where the state, title, and path are not changing?
  2. Should statechange be fired when the state changes, but not the document title or Location?

@dpogue
Copy link

dpogue commented Jun 4, 2020

One additional thing that would be useful to know is whether the navigation is a pop (going back a page in history), a replace (overwriting the current page in history), or a push (adding a new page to history).

It's currently impossible to accurately detect this sort of thing using history.length. There was some previous work around exposing a history.index value, but that didn't go anywhere and would still be less useful than an event.

@frehner
Copy link
Author

frehner commented Jun 4, 2020

A couple questions I'm thinking through with this:

  1. Should statechange be fired when you call history.replaceState(history.state, document.title, location.pathname), where the state, title, and path are not changing?
  2. Should statechange be fired when the state changes, but not the document title or Location?

My naive first impression is yes - it should fire when popstate fires. And then with oldURL / newURL you can just bail early if you don't need to do anything?

One additional thing that would be useful to know is whether the navigation is a pop (going back a page in history), a replace (overwriting the current page in history), or a push (adding a new page to history).

It's currently impossible to accurately detect this sort of thing using history.length. There was some previous work around exposing a history.index value, but that didn't go anywhere and would still be less useful than an event.

Interesting. But it may be a barrier to getting this proposal being adopted if we do co-opt it. Perhaps if an additional proposal could work out the issues with the previously blocked proposal, then we could add additional info to the event at that time or something?

@frehner
Copy link
Author

frehner commented Jun 4, 2020

Small update: I changed the proposed event name to potentially be historychange instead of statechange

I don't have all the context for why popstate is named as it is, but statechange was modeled after it. However, if all things are equal, I think historychange makes more sense as an event name.

@joeldenning
Copy link

joeldenning commented Jun 4, 2020

One additional thing that would be useful to know is whether the navigation is a pop (going back a page in history), a replace (overwriting the current page in history), or a push (adding a new page to history).

I agree - this would be a nice additional property to add to the statechange event. I don't think we'd run into the same problems as the history.index proposal since indicating replace / push / pop is not difficult to achieve in this edge case that seemed to kill the history.index proposal.

@frehner
Copy link
Author

frehner commented Jun 5, 2020

One additional thing that would be useful to know is whether the navigation is a pop (going back a page in history), a replace (overwriting the current page in history), or a push (adding a new page to history).

I agree - this would be a nice additional property to add to the statechange event. I don't think we'd run into the same problems as the history.index proposal since indicating replace / push / pop is not difficult to achieve in this edge case that seemed to kill the history.index proposal.

I'm still somewhat hesitant about this - I think it should be resolved in its own proposal, and then once accepted we can simply add that detail to the event we fire. It seems like a good way of ensuring this proposal doesn't get blocked on issues that may come up with adding that information.

(An addition thought on that as well: what would that detail be if you do location.hash = 'newhash'? The history stack had a new push to it, but also it was a replacement of the existing hash, so would it be a replace event or a push event?)

@joeldenning
Copy link

joeldenning commented Jun 5, 2020

I'm still somewhat hesitant about this - I think it should be resolved in its own proposal, and then once accepted we can simply add that detail to the event we fire. It seems like a good way of ensuring this proposal doesn't get blocked on issues that may come up with adding that information.

If it causes complications, we could remove it? I am not seeing how it would add complications.

(An addition thought on that as well: what would that detail be if you do location.hash = 'newhash'? The history stack had a new push to it, but also it was a replacement of the existing hash, so would it be a replace event or a push event?)

My understanding is that this is a push, since it adds an entry to the stack. You can tell because pressing the back button takes you back to the URL from before the hash change.

@domenic
Copy link
Member

domenic commented Feb 2, 2021

Hey folks! I've been aware of the issues with history for a while, but toward the end of last year fixing them became one of my major projects at work. The conclusion I and others at Google came to over the last month or so is that the the problem goes deeper than just the popstate event being bad: the entire window.history API is subpar.

Our initial proposal for fixing this is at https://github.com/slightlyoff/history_api/blob/master/app_history.md, and I'd love thoughts from you all. In particular, re-reading this thread I see some great feedback on what information would be useful to include in such events, which I'm not sure we've currently captured. In general, I'd appreciate web developers supplying use cases and small code examples I could incorporate into the explainer.

Specifically, the app history API proposal's currententrychange event seems to cover the use cases from your original post. But other parts of the proposal, e.g. the navigate event, might be more suitable for some of the router-like use cases.

Let me know what you think. In particular, if you're interested in exploring this further, please lend your support as a comment (not just thumbs-up!) in WICG/proposals#20 so that the WICG chairs can grant us a repository for incubation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs concrete proposal Moving the issue forward requires someone to figure out a detailed plan needs implementer interest Moving the issue forward requires implementers to express interest topic: history
Development

No branches or pull requests

5 participants