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