recorded decision · public · no signup
Summary
What was chosen
- Vue Router will not modify the user-provided state and pass it as-is to `history.pushState()`.adr ↗
- Vue Router will save the user-provided state to a nested `userState` property within `history.state` to avoid internal conflicts.adr ↗
- Vue Router will allow users to pass a `state` property alongside other navigation properties to persist to `history.state`.adr ↗
- When a `state` property is passed to `router.push()` or `router.replace()`, the router will always create a new navigation.adr ↗
- Vue Router will allow users to read the `history.state` directly at `this.$route.state`.adr ↗
Constraints
- The existing Vue Router makes writing to `history.state` difficult or impossible, especially for same-location navigation.adr ↗
Consequences
- The History API has limitations and browser inconsistencies, which could lead to issues if not properly documented.adr ↗
- Creating a new navigation with state allows multiple history entries for the same URL, enabling different page versions.adr ↗
- Developers are responsible for ensuring the state passed to `router.push()` is serializable and valid for the History API.adr ↗
- The `state` property passed to `router.push()` will be ignored during Server-Side Rendering (SSR) by the Memory History implementation.adr ↗
The recorded why
- Start Date: 2021-10-15
- Target Major Version: Router 4.x
- Reference Issues: https://github.com/vuejs/vue-router/issues/2243
- Implementation PR: (leave this empty)
Summary
- Allow the user to pass a
stateproperty alongsidepath,query, and other properties to persist tohistory.state. - Allow the user to read the
history.statedirectly atthis.$route.state.
Basic example
Programmatic navigation:
router.push({ name: 'Details', state: { showModal: true } })
router.replace({ state: { showModal: true } })
Declarative:
<router-link
:to="{ name: 'Details', state: { showModal: true } }"
>Show Details</router-link>
Motivation
Passing state through the history API is a native feature that is currently hard to use when using Vue Router. While it has its limitations, it has many useful usecases like showing modals and can be use as a source of truth for state that is specific to certain locations and should be persisted across navigations when coming back to a previously visited page.
Currently, this can be achieved most of the times with
// check for the navigation to succeed
if (!(await router.push('/somewhere'))) {
history.replaceState({ ...history.state, ...newState }, '')
}
It currently cannot be achieved if the current location is the same and the only thing we want to do is modify the state.
The router should facilitate using the features of the History API but currently it turns out to make the task of writing to history.state difficult or impossible (e.g. same location navigation)
Detailed design
§ Writing to history.state
Vue Router 4 already uses history.state internally to detect navigation direction and revert UI initiated navigations such as the back and forward button. In order to not interfere with the information stored by it, it should save the state passed by the user to a nested property:
// somewhere inside the router code
history.pushState({ ...routerState, userState: state }, '', url)
Duplicated navigations
By default, the router avoids any duplicated navigation (e.g. clicking multiple times on the same link) or calling router.push('/somewhere') when we are already at /somewhere. When state is passed to router.push() (or router.replace()), the router should always create a new navigation. This creates a hidden way to force a navigation to the same location and also the possibility to have multiple entries on the history stack that point to the same URL but this should be fine as they should contain different state.
- User goes to
/search - User clicks on button that does
router.push({ state: { searchResults: [] }}) - User stays at
/searchbut the page can use the passed state to display a different version - User clicks the back button, they stay at
/searchbut see a different version of the page
Invalid state properties
Since the state must be serializable, some key or property values are invalid and should be avoided (e.g. DOM nodes or complex objects, functions, Symbols). Vue Router won't touch the state given by the user and pass it as is to history.pushState(). The developer is responsible for this and must be aware that browsers might treat some Data Structures differently.
SSR
Since this feature only works with the History API, any given state property passed to router.push() will be ignored during SSR by the Memory History implementation.
§ Reading the state
It would be convenient to be able to read the history.state directly from the current route because that would make it reactive and allow watching or creating computed properties based on it:
const route = useRoute()
const showModal = computed(() => route.state.showModal)
<Modal v-if="$route.state.showModal" />
For convenience reasons, the route.state property should be an empty object by default.
This introduces a new TS interface to represent the current location as the History API only allows reading from the current entry. Therefore from.state is unavailable in Navigation guards while to.state can be available:
router.beforeEach((to, from) => {
to.state // undefined | unknown
from.state // TS Error property doesn't exist
})
Drawbacks
- The History API has its own limitations and inconsistencies among browsers and they sometimes vary (e.g. the way state is persisted to disk and how objects are cloned). This could be a foot shot if not documented properly in terms of usage. For instance, it should be avoided to store big amounts of data that should go in component state or in a store
- Making
route.stateretrieve onlyhistory.state.userStateallows us to not expose the information stored by the router (since it's not public API) but also doesn't allow information stored in thehistory.stateby other libraries. I think this is okay because the user can create a computed property to read from those properties withcomputed(() => route && history.state.myOwnProperty).
Alternatives
- The
route.stateproperty could beundefinedwhen not set. - Letting
route.statebe the wholehistory.stateinstead of what the user passed
Adoption strategy
Currently Vue Router 4 allows passing a state property to router.push() but the API is not documented and marked as @internal and should therefore not be used. Other than that, this API is an addition.