Vue-router: Allow a user to read and write state on history entries (history.state)

Created on 5 Jun 2018  路  9Comments  路  Source: vuejs/vue-router

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.

discussion needs RFC

Most helpful comment

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.

All 9 comments

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

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.

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.

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.

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.

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 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

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.

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

saman picture saman  路  3Comments

alexstanbury picture alexstanbury  路  3Comments

posva picture posva  路  3Comments

druppy picture druppy  路  3Comments

lbineau picture lbineau  路  3Comments