vue-router icon indicating copy to clipboard operation
vue-router copied to clipboard

Allow a user to read and write state on history entries (history.state)

Open crswll opened this issue 6 years ago • 19 comments

What problem does this feature solve?

Mobile apps that want to save scroll position for elements besides window.

This would also allow the user to save whatever they want to history state. A project I'm working on now needs to keep track of what's "focused" per page and this seems like the correct place for it. It could also be a nice place for temporary form data. I'm sure there's a lot of use cases outside of this.

What does the proposed API look like?

I'm still thinking about it, though I think someone else thinking about it would be better. :)

I checked out react-router a bit and it looks like we can make state part of the Location object, so we can set it in $router.push|replace({...}) or next({...}) calls. They use an additional argument but we might not need to?

I took a quick look at react-router and it seems to allow for this: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/history.md

Also, this seems to talk directly to the situation we're facing with scrolling: https://reacttraining.com/react-router/web/guides/scroll-restoration

Also some additional reading: https://github.com/ReactTraining/history#properties

Hopefully this is useful and doesn't seem like useless rambling.

crswll avatar Jun 05 '18 15:06 crswll

My app relies on being able to associate extra state in the history that isn't in the URL and this prevents me from being able to use vue-router

react-router uses history, which does something like:

history.replaceState(
  {  key: createKey(),  state: 'user-provided state' },
  null,
);

and provides access to location something like:

const location = {
  pathname: window.location.pathname,
  search: window.location.search,
  hash: window.location.hash,
  state: history.state.state,
}

users aren't expected to access history.state directly, though I suppose you could

jnields avatar Sep 04 '18 19:09 jnields

This would be extremely useful as a way to pass data to the scrollBehavior handler to allow different router-links that share the same to= string/object so that they can differ in regards to scrollBehavior.

719media avatar Apr 22 '19 03:04 719media

To workaround this, I currently have to track changes to the $route globally, inspect and copy values from window.history.state, then use window.history.replaceState() to push the new application state into the window history. It would be helpful if this is done within the Router framework.

J-Rojas avatar Nov 09 '19 19:11 J-Rojas

There are some scenarios where this is impossible to workaround. Our app has some popups that need to close on the device back press. Right now we add a query in the URL to do that but as the number of popups increases, our URLs would become nasty.

The one and the only way to show a popup like a native app is to store its state in the history object. This way we can push the same URL and store a flag for the popup in the history so that on the device back it can be used to close the popup again. This not only solves the problem of closing a popup on the device back it will also persist the state on page refresh so that the popup can open up again without relying on the URL.

There are cases where opening a popup with a URL change is the correct way but there are other cases too where a URL change is completely unnecessary.

Take a look at this page. Open it in a mobile and click on view similar.

They were able to do it because they used React.

And we are not able to do it because of the default behaviour of vue-router.

jimut avatar Feb 24 '20 15:02 jimut

FYI since 3.1.4 you can overwrite the history state using replaceState and vue router won't replace it anymore. This feature request should go through the RFC process to go through the thinking process of pros, cons, alternatives and gather more feedback.

posva avatar Feb 24 '20 20:02 posva

Yeah, I can see that router.replace doesn't override the state but it will not help to solve this use case.

The only half ended solution that I can find is to use history.pushState directly to push the same route with a flag in the state when the popup is opened so that it gets closed on device back.

But still, the problem of persisting the popup state on page refresh will not get solved because the function setupScroll is called on initialization of vue-router and it replaces the state like this.

window.history.replaceState({ key: getStateKey() }, '', absolutePath)

Maybe here if you could have written the logic of not replacing the state like this.

window.history.replaceState({ ...window.history.state, key: getStateKey() }, '', absolutePath)

We could have implemented a full-fledged solution by now.

jimut avatar Feb 25 '20 10:02 jimut

@jimut I will definitely take a PR to improve that! It's part of https://github.com/vuejs/vue-router/issues/3006 It shouldn't be a very hard PR to make. It does require an e2e test?

edit: fixed that one and released v3.1.6

posva avatar Feb 25 '20 12:02 posva

Thank you so much for the quick fix.

Hopefully in 3.2 we will be able to pass state objects in push/replace methods and get to from the route object maybe.

jimut avatar Feb 26 '20 13:02 jimut

As said, passing state through push/replace needs to go through the RFC process but right now you can just do

await router.push('/somewhere')
history.replaceState({ ...history.state, ...newState }, '')

To add state to the current history entry after a navigation

posva avatar Feb 26 '20 14:02 posva

I'd like to throw in my thoughts on this as you mentioned this is an RFC now. History state is a fantastic feature that would really improve vue router. I came across this issue when I ended up spending a few hours working on figuring out a way to get a workflow done without it.

I was trying to figure out a way to easily and consistently make it so I could always return to the same page the user left from. (or the default if the user navigated directly to the Other route).

<v-btn :to="{name: 'Other', state: {prevRoute: $route.fullPath}}">btn</v-btn>

This would be paired with a corresponding button on the other page. That would've been all the code I needed to make the feature work the way I wanted and was the first thing I tried.

<v-btn :to="$route.state?.prevRoute || '/overview/main' ">Return</v-btn>

After finding this thread, I could've done the same awaiting programatic navigation and then a manual history.replaceState, but that would've made it so that the link to go to Other wouldn't be a proper link i.e. user wouldn't be able to see the url ahead of time.


Instead I spent a couple hours searching through the api and trying things out ineffectively. Like the meta values have to be applied via the route itself, which meant it wasn't helpful here. I could've used query params, but I didn't want to crowd the ending url with this metadata as it is more of a convenience feature and might be confusing for users.

What I ended up with was setting up a beforeRouteEnter guard on the Other component where I set a data property via the callback on next as was pointed out https://stackoverflow.com/a/53789212/13175138.

Which meant that I had to check to make sure that navigation to that route was coming from the overview page and then set the property only in that instance. And of course, if the page is refreshed, that data is lost since it's store in JS.

What I ended up with was:

  beforeRouteEnter(to, from, next) {
    next((vm) => {
      if (from.name === 'Overview') {
        vm.prevRoute = from.fullPath;
      }
    });
  },

In react-router, the API already exists where you can just put a state on a Link and it works: https://reactrouter.com/web/api/Link/to-object.

<Link to={{pathname: '/other', state: {prevRoute: location.pathname}}} />

I'm well aware that vue router and react router are very different in their philosophies, but I still feel like this should be standard in any history api based router due to both the convenience and the ability for state to be tied to a particular location in the browser history. The latter definitively adding a ton of possibilities that would otherwise require much less clean approaches.

ZachHaber avatar Aug 20 '21 16:08 ZachHaber

Published an RFC for this: https://github.com/vuejs/rfcs/discussions/400

posva avatar Oct 15 '21 14:10 posva

Fixed in version 4.1!!!

zenflow avatar Jul 08 '22 00:07 zenflow

Just tried it in 4.1. We can now set state in push/replace, but it seems that that data is not attached to the vue-router location objects. We have to get it from window.history directly, which may necessitate jumping through hoops in certain situations. I hope this is planned to be added soon. React router has had both of these features since at least v3 (~5 years old).

vincerubinetti avatar Jul 30 '22 15:07 vincerubinetti

Am I right in thinking there is no way to have state specified once and applied to all navigations?

leegee avatar May 18 '23 12:05 leegee

I hope this issue gets more traction because I am having quite a difficult time saving & restoring state

AlejandroAkbal avatar Jun 22 '23 15:06 AlejandroAkbal

I made a fork off of the Vue 2 router to solve this issue for me: https://github.com/ZachHaber/vue-router-state. It allows passing in state as a property on RouteLinks, push, and replace. You can get the state off of the $route object (and via the composition api 'vue-router-2-state/composables'). It should be a drop-in replacement with better typescript support than the original.

ZachHaber avatar Nov 30 '23 15:11 ZachHaber

I made a fork off of the Vue 2 router to solve this issue for me: https://github.com/ZachHaber/vue-router-state. It allows passing in state as a property on RouteLinks, push, and replace. You can get the state off of the $route object (and via the composition api 'vue-router-2-state/composables'). It should be a drop-in replacement with better typescript support than the original.

Interesting, thanks for sharing. Maybe put the docs in the README.md?

leegee avatar Nov 30 '23 15:11 leegee

Interesting, thanks for sharing. Maybe put the docs in the README.md?

The docs on it are live and updated a little with the composables (from the vue-router 4 docs, as those still apply) as well as the history state parts.

ZachHaber avatar Dec 01 '23 13:12 ZachHaber

Fixed in version 4.1!!!

So I guess not really? As per the RFC:

There are plans for a similar API for params, query, and state 🙂

Currently the e2e example sets up a ref() to history.state but surely this should be provided by vue-router? That would also give the framework an opportunity to remove (hide) internal state from users, which is probably desirable?

Cheaterman avatar Feb 22 '24 15:02 Cheaterman