Skip to content

Commit e03cfba

Browse files
committed
docs: navigation guards
1 parent de2554c commit e03cfba

File tree

2 files changed

+108
-43
lines changed

2 files changed

+108
-43
lines changed

docs/guide/advanced/navigation-guards.md

Lines changed: 106 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,41 @@ You can register global before guards using `router.beforeEach`:
99
```js
1010
const router = createRouter({ ... })
1111

12-
router.beforeEach((to, from, next) => {
12+
router.beforeEach((to, from) => {
1313
// ...
14-
next()
14+
// explicitly return false to cancel the navigation
15+
return false
1516
})
1617
```
1718

1819
Global before guards are called in creation order, whenever a navigation is triggered. Guards may be resolved asynchronously, and the navigation is considered **pending** before all hooks have been resolved.
1920

20-
Every guard function receives three arguments:
21+
Every guard function receives two arguments:
2122

22-
- **`to`**: the target [Normalized RouteLocation](../../api/#the-route-object) being navigated to.
23+
- **`to`**: the target route ___location [in a normalized format](../../api/#the-route-object) being navigated to.
24+
- **`from`**: the current route ___location [in a normalized format](../../api/#the-route-object) being navigated away from.
2325

24-
- **`from`**: the current route ___location (same type as `to`) being navigated away from.
26+
And can optionally return any of the following values:
2527

26-
- **`next`**: a function that **must be called to resolve** the hook. The action depends on the arguments provided to `next`:
28+
- `false`: cancel the current navigation. If the browser URL was changed (either manually by the user or via back button), it will be reset to that of the `from` route.
29+
- A [Route Location](../api#route-___location): Redirect to a different ___location by passing a route ___location as if you were calling [`router.push()`](../api#router-push), which allows you to pass options like `replace: true` or `name: 'home'`. The current navigation is dropped and a new one is created with the same `from`.
2730

28-
- **`next()`**: move on to the next hook in the pipeline. If no hooks are left, the navigation is **validated**.
31+
It's also possible to throw an `Error` if an unexpected situation was met. This will also cancel the navigation and call any callback registered via [`router.onError()`](../api#router-onerror).
2932

30-
- **`next(false)`**: cancel the current navigation. If the browser URL was changed (either manually by the user or via back button), it will be reset to that of the `from` route.
33+
If nothing, `undefined` or `true` is returned, **the navigation is validated**, and the next navigation guard is called.
3134

32-
- **`next('/')` or `next({ path: '/' })`**: redirect to a different ___location. The current navigation will be dropped and a new one will be initiated. **You can pass any route ___location object** to `next`, which allows you to specify options like `replace: true`, `name: 'home'` and any option used in [`router-link`'s `to` prop](../../api/#to) or [`router.push`](../../api/#router-push)
35+
All of the the things above **work the same way with `async` functions** and Promises:
3336

34-
- **`next(error)`**: (2.4.0+) if the argument passed to `next` is an instance of `Error`, the navigation will be canceled and the error will be passed to callbacks registered via [`router.onError()`](../../api/#router-onerror). The same happens if an error is directly thrown.
37+
```js
38+
router.beforeEach(async (to, from) => {
39+
// canUserAccess() returns `true` or `false`
40+
return await canUserAccess(to)
41+
})
42+
```
43+
44+
### Optional third argument `next`
3545

36-
::: tip Note
37-
For all guards, **make sure that the `next` function is called exactly once** in any given pass through the navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors. Here is an example of redirecting to user to `/login` if they are not authenticated:
46+
In previous versions of Vue Router, it was also possible to use a _third argument_ `next`, this was a common source of mistakes and went through an [RFC](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0037-router-return-guards.md#motivation) to remove it. However, it is still supported, meaning you can pass a third argument to any navigation guard. In that case, **you must call `next` exactly once** in any given pass through a navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors. Here is **a bad example** of redirecting to user to `/login` if they are not authenticated:
3847

3948
```js
4049
// BAD
@@ -45,6 +54,8 @@ router.beforeEach((to, from, next) => {
4554
})
4655
```
4756

57+
Here is the correct version:
58+
4859
```js
4960
// GOOD
5061
router.beforeEach((to, from, next) => {
@@ -53,34 +64,30 @@ router.beforeEach((to, from, next) => {
5364
})
5465
```
5566

56-
:::
57-
5867
## Global Resolve Guards
5968

60-
You can register a global guard with `router.beforeResolve`. This is similar to `router.beforeEach` because it triggers on **every navigation**, but resolve guards are called right before the navigation is confirmed, **after all in-component guards and async route components are resolved**. This is the ideal spot to fetch data or do any other operation that you want to avoid doing if the user cannot enter a page. It's also very easy to combine with [`meta` fields](./meta.md) to create a [generic fetching mechanism](../../cookbook/generic-data-fetching.md):
69+
You can register a global guard with `router.beforeResolve`. This is similar to `router.beforeEach` because it triggers on **every navigation**, but resolve guards are called right before the navigation is confirmed, **after all in-component guards and async route components are resolved**. Here is an example that ensures the user has given access to the Camera for routes that [have defined a custom meta](#TODO) property `requiresCamera`:
6170

6271
```js
63-
router.beforeResolve(async (to, from, next) => {
72+
router.beforeResolve(async to => {
6473
if (to.meta.requiresCamera) {
6574
try {
6675
await askForCameraPermission()
67-
next()
6876
} catch (error) {
6977
if (error instanceof NotAllowedError) {
7078
// ... handle the error and then cancel the navigation
71-
next(false)
79+
return false
7280
} else {
7381
// unexpected error, cancel the navigation and pass the error to the global handler
74-
next(error)
82+
throw error
7583
}
7684
}
77-
} else {
78-
// make sure to always call `next`
79-
next()
8085
}
8186
})
8287
```
8388

89+
`router.beforeResolve` is the ideal spot to fetch data or do any other operation that you want to avoid doing if the user cannot enter a page. It's also very easy to combine with [`meta` fields](./meta.md) to create a [generic fetching mechanism](../../cookbook/generic-data-fetching.md)
90+
8491
## Global After Hooks
8592

8693
You can also register global after hooks, however unlike guards, these hooks do not get a `next` function and cannot affect the navigation:
@@ -93,6 +100,16 @@ router.afterEach((to, from) => {
93100

94101
They are useful for analytics, [changing the title of the page](../../cookbook/page-title.md), [accessibility](../../cookbook/announcing-navigation.md) and many other things.
95102

103+
They also reflect [navigation failures](./navigation-failures.md) as the third argument:
104+
105+
```js
106+
router.afterEach((to, from, failure) => {
107+
if (!failure) sendToAnalytics(to.fullPath)
108+
})
109+
```
110+
111+
Learn more about navigation failures on [its guide](./navigation-failures.md).
112+
96113
## Per-Route Guard
97114

98115
You can define `beforeEnter` guards directly on a route's configuration object:
@@ -102,19 +119,51 @@ const routes = [
102119
{
103120
path: '/users/:id',
104121
component: UserDetails,
105-
beforeEnter: (to, from, next) => {
106-
// ...
107-
next()
108-
}
109-
}
122+
beforeEnter: (to, from) => {
123+
// reject the navigation
124+
return false
125+
},
126+
},
110127
]
111128
```
112129

113130
`beforeEnter` guards **only trigger when entering the route**, they don't trigger when the `params`, `query` or `hash` change e.g. going from `/users/2` to `/users/3` or going from `/users/2#info` to `/users/2#projects`. They are only triggered when navigating **from a different** route.
114131

132+
You can also pass an array of functions to `beforeEnter`, this is useful when reusing guards for different routes:
133+
134+
```js
135+
function removeQueryParams(to) {
136+
if (Object.keys(to.query).length)
137+
return { path: to.path, query: {}, hash: to.hash }
138+
}
139+
140+
function removeHash(to) {
141+
if (to.hash) return { path: to.path, query: to.query, hash: '' }
142+
}
143+
144+
const routes = [
145+
{
146+
path: '/users/:id',
147+
component: UserDetails,
148+
beforeEnter: [removeQueryParams, removeHash],
149+
},
150+
{
151+
path: '/about',
152+
component: UserDetails,
153+
beforeEnter: [removeQueryParams],
154+
},
155+
]
156+
```
157+
158+
Note it is possible to achieve a similar behavior by using [route meta fields](./meta.md) and [global navigation guards](#global-before-guards).
159+
115160
## In-Component Guards
116161

117-
Finally, you can directly define route navigation guards inside route components (the ones passed to the router configuration) with the following options:
162+
Finally, you can directly define route navigation guards inside route components (the ones passed to the router configuration)
163+
164+
### Using the options API
165+
166+
You can add the following options to route components:
118167

119168
- `beforeRouteEnter`
120169
- `beforeRouteUpdate`
@@ -123,24 +172,24 @@ Finally, you can directly define route navigation guards inside route components
123172
```js
124173
const UserDetails = {
125174
template: `...`,
126-
beforeRouteEnter(to, from, next) {
175+
beforeRouteEnter(to, from) {
127176
// called before the route that renders this component is confirmed.
128177
// does NOT have access to `this` component instance,
129178
// because it has not been created yet when this guard is called!
130179
},
131-
beforeRouteUpdate(to, from, next) {
180+
beforeRouteUpdate(to, from) {
132181
// called when the route that renders this component has changed,
133182
// but this component is reused in the new route.
134183
// For example, given a route with params `/users/:id`, when we
135184
// navigate between `/users/1` and `/users/2`, the same `UserDetails` component instance
136185
// will be reused, and this hook will be called when that happens.
137186
// Because the component is mounted while this happens, the navigation guard has access to `this` component instance.
138187
},
139-
beforeRouteLeave(to, from, next) {
188+
beforeRouteLeave(to, from) {
140189
// called when the route that renders this component is about to
141190
// be navigated away from.
142191
// As with `beforeRouteUpdate`, it has access to `this` component instance.
143-
}
192+
},
144193
}
145194
```
146195

@@ -159,23 +208,39 @@ beforeRouteEnter (to, from, next) {
159208
Note that `beforeRouteEnter` is the only guard that supports passing a callback to `next`. For `beforeRouteUpdate` and `beforeRouteLeave`, `this` is already available, so passing a callback is unnecessary and therefore _not supported_:
160209

161210
```js
162-
beforeRouteUpdate (to, from, next) {
211+
beforeRouteUpdate (to, from) {
163212
// just use `this`
164213
this.name = to.params.name
165-
next()
166214
}
167215
```
168216

169-
The **leave guard** is usually used to prevent the user from accidentally leaving the route with unsaved edits. The navigation can be canceled by calling `next(false)`.
217+
The **leave guard** is usually used to prevent the user from accidentally leaving the route with unsaved edits. The navigation can be canceled by returning `false`.
170218

171219
```js
172-
beforeRouteLeave (to, from, next) {
220+
beforeRouteLeave (to, from) {
173221
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
174-
if (answer) {
175-
next()
176-
} else {
177-
next(false)
178-
}
222+
if (!answer) return false
223+
}
224+
```
225+
226+
### Using the composition API
227+
228+
If you are writing your component using the [composition API and a `setup` function](https://v3.vuejs.org/guide/composition-api-setup.html#setup), you can add update and leave guards:
229+
230+
```js
231+
import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'
232+
233+
const UserDetails = {
234+
template: `...`,
235+
setup() {
236+
onBeforeRouteUpdate((to, from) => {
237+
// same as the beforeRouteUpdate option but with no access to `this`
238+
})
239+
240+
onBeforeRouteLeave((to, from) => {
241+
// same as the beforeRouteLeave option but with no access to `this`
242+
})
243+
},
179244
}
180245
```
181246

playground/views/GuardedWithLeave.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export default defineComponent({
1616
1717
setup() {
1818
console.log('setup in cant leave')
19-
onBeforeRouteLeave(function(to, from, next) {
20-
if (window.confirm()) next()
19+
onBeforeRouteLeave(function (to, from, next) {
20+
if (window.confirm('Do you really want to leave?')) next()
2121
else {
2222
// @ts-ignore
2323
this.tries++

0 commit comments

Comments
 (0)