Skip to content

Commit 48104bc

Browse files
authored
Allow router.push() to pass state (#401)
1 parent deb9a60 commit 48104bc

File tree

1 file changed

+112
-0
lines changed

1 file changed

+112
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
- Start Date: 2021-10-15
2+
- Target Major Version: Router 4.x
3+
- Reference Issues: https://github.com/vuejs/vue-router/issues/2243
4+
- Implementation PR: (leave this empty)
5+
6+
# Summary
7+
8+
- Allow the user to pass a `state` property alongside `path`, `query`, and other properties to persist to `history.state`.
9+
- Allow the user to read the `history.state` directly at `this.$route.state`.
10+
11+
# Basic example
12+
13+
Programmatic navigation:
14+
15+
```js
16+
router.push({ name: 'Details', state: { showModal: true } })
17+
router.replace({ state: { showModal: true } })
18+
```
19+
20+
Declarative:
21+
22+
```vue
23+
<router-link
24+
:to="{ name: 'Details', state: { showModal: true } }"
25+
>Show Details</router-link>
26+
```
27+
28+
# Motivation
29+
30+
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.
31+
32+
Currently, this can be achieved most of the times with
33+
34+
```js
35+
// check for the navigation to succeed
36+
if (!(await router.push('/somewhere'))) {
37+
history.replaceState({ ...history.state, ...newState }, '')
38+
}
39+
```
40+
41+
It currently cannot be achieved if the current location is the same and the only thing we want to do is _modify_ the state.
42+
43+
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)
44+
45+
# Detailed design
46+
47+
## Writing to `history.state`
48+
49+
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:
50+
51+
```js
52+
// somewhere inside the router code
53+
history.pushState({ ...routerState, userState: state }, '', url)
54+
```
55+
56+
### Duplicated navigations
57+
58+
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.
59+
60+
1. User goes to `/search`
61+
2. User clicks on button that does `router.push({ state: { searchResults: [] }})`
62+
3. User stays at `/search` but the page can use the passed state to display a different version
63+
4. User clicks the _back_ button, they stay at `/search` but see a different version of the page
64+
65+
### Invalid state properties
66+
67+
Since [the state must be serializable](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState), 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.
68+
69+
### SSR
70+
71+
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.
72+
73+
## Reading the state
74+
75+
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:
76+
77+
```js
78+
const route = useRoute()
79+
80+
const showModal = computed(() => route.state.showModal)
81+
```
82+
83+
```vue
84+
<Modal v-if="$route.state.showModal" />
85+
```
86+
87+
For convenience reasons, the `route.state` property should be an empty object by default.
88+
89+
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:
90+
91+
```ts
92+
router.beforeEach((to, from) => {
93+
to.state // undefined | unknown
94+
from.state // TS Error property doesn't exist
95+
})
96+
```
97+
98+
# Drawbacks
99+
100+
- 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
101+
- Making `route.state` retrieve only `history.state.userState` allows us to not expose the information stored by the router (since it's not public API) but also doesn't allow information stored in the `history.state` by other libraries. I think this is okay because the user can create a computed property to read from those properties with `computed(() => route && history.state.myOwnProperty)`.
102+
103+
# Alternatives
104+
105+
- The `route.state` property could be `undefined` when not set.
106+
- Letting `route.state` be the whole `history.state` instead of what the user passed
107+
108+
# Adoption strategy
109+
110+
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.
111+
112+
# Unresolved questions

0 commit comments

Comments
 (0)