Navigation Guards
Control and protect your routes with powerful navigation guards that can intercept, validate, and redirect navigation attempts.
Introduction
Navigation guards are functions that allow you to control route navigation by intercepting route changes. They can be used for authentication, authorization, data validation, and other navigation-related logic.
Types of Navigation Guards
Vue Router provides several types of navigation guards:
- Global Guards: Apply to all routes
- Per-Route Guards: Apply to specific routes
- In-Component Guards: Defined within components
Global Before Guards
Basic Usage
Register global before guards using router.beforeEach():
javascript
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
// your routes
]
})
router.beforeEach((to, from) => {
// Guard logic here
console.log('Navigating from', from.path, 'to', to.path)
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Authentication Example
javascript
router.beforeEach(async (to, from) => {
// Check if route requires authentication
if (to.meta.requiresAuth && !isAuthenticated()) {
// Redirect to login page
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
// Route configuration with meta field
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: Login
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Return Values
Guards can return different values to control navigation:
javascript
router.beforeEach((to, from) => {
// Allow navigation (default)
return true
// or simply return nothing/undefined
// Cancel navigation
return false
// Redirect to different route
return '/login'
// or
return { name: 'Login' }
// or
return { path: '/login', query: { redirect: to.fullPath } }
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Async Guards
javascript
router.beforeEach(async (to, from) => {
try {
// Async operation
const user = await getCurrentUser()
if (to.meta.requiresAuth && !user) {
return { name: 'Login' }
}
// Check user permissions
if (to.meta.requiresRole && !user.roles.includes(to.meta.requiresRole)) {
return { name: 'Unauthorized' }
}
} catch (error) {
console.error('Authentication check failed:', error)
return { name: 'Error' }
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Error Handling
javascript
router.beforeEach(async (to, from) => {
try {
await validateRoute(to)
} catch (error) {
// Throwing an error cancels navigation and triggers router.onError()
throw new Error(`Navigation failed: ${error.message}`)
}
})
// Handle navigation errors
router.onError((error) => {
console.error('Navigation error:', error)
// Show error message to user
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Global Resolve Guards
Execute logic right before navigation is confirmed:
javascript
router.beforeResolve(async (to) => {
// Perfect place for data fetching or final checks
if (to.meta.requiresCamera) {
try {
await requestCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
return false // Cancel navigation
}
throw error // Unexpected error
}
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Data Fetching Example
javascript
router.beforeResolve(async (to) => {
// Fetch critical data before entering route
if (to.meta.preloadData) {
try {
const data = await fetchRouteData(to.params)
// Store data in route meta or global store
to.meta.data = data
} catch (error) {
// Handle data loading errors
return { name: 'Error', params: { error: error.message } }
}
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Global After Hooks
Execute logic after navigation is confirmed:
javascript
router.afterEach((to, from, failure) => {
// Navigation completed successfully
if (!failure) {
// Analytics tracking
trackPageView(to.fullPath)
// Update page title
document.title = to.meta.title || 'My App'
// Accessibility announcements
announcePageChange(to.meta.title)
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Analytics and Tracking
javascript
router.afterEach((to, from, failure) => {
if (!failure) {
// Google Analytics
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.fullPath
})
// Custom analytics
analytics.track('Page View', {
path: to.fullPath,
title: to.meta.title,
referrer: from.fullPath
})
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Global Injections in Guards
Since Vue 3.3, you can use dependency injection in guards:
javascript
// main.js
const app = createApp(App)
app.provide('analytics', analyticsService)
app.provide('auth', authService)
// router.js
router.beforeEach((to, from) => {
const auth = inject('auth')
const analytics = inject('analytics')
// Use injected services
if (to.meta.requiresAuth && !auth.isAuthenticated()) {
return { name: 'Login' }
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Pinia Store Integration
javascript
import { useAuthStore } from '@/stores/auth'
router.beforeEach((to, from) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return { name: 'Login' }
}
})1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Per-Route Guards
Basic Per-Route Guard
javascript
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: (to, from) => {
// Only admins can access
if (!isAdmin()) {
return { name: 'Unauthorized' }
}
}
}
]1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Multiple Guards
javascript
// Reusable guard functions
function requireAuth(to, from) {
if (!isAuthenticated()) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
}
function requireAdmin(to, from) {
if (!isAdmin()) {
return { name: 'Unauthorized' }
}
}
function logAccess(to, from) {
console.log(`Accessing ${to.path}`)
}
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: [requireAuth, requireAdmin, logAccess]
},
{
path: '/profile',
component: UserProfile,
beforeEnter: [requireAuth, logAccess]
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Conditional Guards
javascript
function createRoleGuard(requiredRole) {
return (to, from) => {
const user = getCurrentUser()
if (!user || !user.roles.includes(requiredRole)) {
return { name: 'Unauthorized' }
}
}
}
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: createRoleGuard('admin')
},
{
path: '/moderator',
component: ModeratorPanel,
beforeEnter: createRoleGuard('moderator')
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Nested Route Guards
javascript
const routes = [
{
path: '/user',
component: UserLayout,
beforeEnter: requireAuth, // Applies to all child routes
children: [
{
path: 'profile',
component: UserProfile
// Inherits parent's beforeEnter
},
{
path: 'settings',
component: UserSettings,
beforeEnter: (to, from) => {
// Additional guard for settings
if (!canEditSettings()) {
return false
}
}
}
]
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
In-Component Guards
Options API
vue
<script>
export default {
name: 'UserProfile',
beforeRouteEnter(to, from) {
// Called before component is created
// No access to `this`
console.log('Entering user profile')
// Can access instance via callback
return (vm) => {
vm.loadUserData(to.params.id)
}
},
beforeRouteUpdate(to, from) {
// Called when route changes but component is reused
// Has access to `this`
this.loadUserData(to.params.id)
},
beforeRouteLeave(to, from) {
// Called when leaving the route
// Has access to `this`
if (this.hasUnsavedChanges) {
const answer = window.confirm(
'You have unsaved changes. Are you sure you want to leave?'
)
if (!answer) return false
}
},
methods: {
loadUserData(userId) {
// Load user data
}
}
}
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Composition API
vue
<script setup>
import { ref, onMounted } from 'vue'
import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'
const userData = ref(null)
const hasUnsavedChanges = ref(false)
// Load initial data
onMounted(() => {
loadUserData()
})
// Handle route updates
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
await loadUserData(to.params.id)
}
})
// Handle route leave
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm(
'You have unsaved changes. Are you sure you want to leave?'
)
if (!answer) return false
}
})
async function loadUserData(userId = route.params.id) {
try {
userData.value = await fetchUser(userId)
} catch (error) {
console.error('Failed to load user data:', error)
}
}
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Advanced Component Guards
vue
<script setup>
import { ref, computed } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useConfirmDialog } from '@/composables/useConfirmDialog'
const formData = ref({})
const originalData = ref({})
const { confirm } = useConfirmDialog()
const hasChanges = computed(() => {
return JSON.stringify(formData.value) !== JSON.stringify(originalData.value)
})
onBeforeRouteLeave(async (to, from) => {
if (hasChanges.value) {
const shouldLeave = await confirm({
title: 'Unsaved Changes',
message: 'You have unsaved changes. Do you want to save before leaving?',
actions: ['Save & Leave', 'Leave without Saving', 'Cancel']
})
if (shouldLeave === 'Save & Leave') {
await saveForm()
return true
} else if (shouldLeave === 'Leave without Saving') {
return true
} else {
return false // Cancel navigation
}
}
})
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Navigation Resolution Flow
Understanding the complete navigation flow:
1. Navigation triggered
2. Call beforeRouteLeave guards in deactivated components
3. Call global beforeEach guards
4. Call beforeRouteUpdate guards in reused components
5. Call beforeEnter in route configs
6. Resolve async route components
7. Call beforeRouteEnter in activated components
8. Call global beforeResolve guards
9. Navigation is confirmed
10. Call global afterEach hooks
11. DOM updates triggered
12. Call callbacks passed to next in beforeRouteEnter guards1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Flow Visualization
javascript
// Example demonstrating the flow
router.beforeEach((to, from) => {
console.log('1. Global beforeEach')
})
router.beforeResolve((to, from) => {
console.log('2. Global beforeResolve')
})
router.afterEach((to, from) => {
console.log('3. Global afterEach')
})
// In component
export default {
beforeRouteEnter(to, from) {
console.log('Component beforeRouteEnter')
},
beforeRouteUpdate(to, from) {
console.log('Component beforeRouteUpdate')
},
beforeRouteLeave(to, from) {
console.log('Component beforeRouteLeave')
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Advanced Patterns
Conditional Navigation
javascript
router.beforeEach(async (to, from) => {
// Skip guards for certain routes
if (to.meta.skipGuards) {
return true
}
// Different logic based on route type
if (to.meta.type === 'public') {
return true
} else if (to.meta.type === 'protected') {
return await checkAuthentication()
} else if (to.meta.type === 'admin') {
return await checkAdminAccess()
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Progressive Enhancement
javascript
router.beforeEach(async (to, from) => {
// Basic check first
if (!isAuthenticated()) {
return { name: 'Login' }
}
// Progressive checks
if (to.meta.requiresVerification && !isEmailVerified()) {
return { name: 'VerifyEmail' }
}
if (to.meta.requiresSubscription && !hasActiveSubscription()) {
return { name: 'Subscribe' }
}
if (to.meta.requiresRole && !hasRequiredRole(to.meta.requiresRole)) {
return { name: 'Unauthorized' }
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Guard Composition
javascript
// Composable guards
function useAuthGuard() {
return (to, from) => {
if (!isAuthenticated()) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
}
}
function useRoleGuard(role) {
return (to, from) => {
if (!hasRole(role)) {
return { name: 'Unauthorized' }
}
}
}
function useSubscriptionGuard() {
return (to, from) => {
if (!hasActiveSubscription()) {
return { name: 'Subscribe' }
}
}
}
// Combine guards
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: [
useAuthGuard(),
useRoleGuard('admin'),
useSubscriptionGuard()
]
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Best Practices
1. Keep Guards Simple
javascript
// ✅ Good: Simple and focused
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login' }
}
})
// ❌ Avoid: Complex logic in guards
router.beforeEach(async (to, from) => {
// Too much logic here
const user = await getUser()
const permissions = await getPermissions(user.id)
const subscription = await getSubscription(user.id)
// ... more complex logic
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2. Use Meta Fields
javascript
// ✅ Good: Use meta fields for configuration
const routes = [
{
path: '/admin',
component: AdminPanel,
meta: {
requiresAuth: true,
requiresRole: 'admin',
title: 'Admin Panel'
}
}
]
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login' }
}
if (to.meta.requiresRole && !hasRole(to.meta.requiresRole)) {
return { name: 'Unauthorized' }
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3. Handle Errors Gracefully
javascript
router.beforeEach(async (to, from) => {
try {
await validateAccess(to)
} catch (error) {
console.error('Guard error:', error)
// Provide fallback behavior
if (error.code === 'NETWORK_ERROR') {
// Allow navigation but show warning
showNetworkWarning()
return true
} else {
// Block navigation for other errors
return { name: 'Error' }
}
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4. Provide User Feedback
javascript
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
// Show helpful message
showMessage('Please log in to access this page', 'warning')
return { name: 'Login', query: { redirect: to.fullPath } }
}
})1
2
3
4
5
6
7
2
3
4
5
6
7
Testing Navigation Guards
Unit Testing
javascript
import { createRouter, createMemoryHistory } from 'vue-router'
import { beforeEach } from '@/router/guards'
describe('Navigation Guards', () => {
let router
beforeEach(() => {
router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/admin', component: Admin, meta: { requiresAuth: true } }
]
})
router.beforeEach(beforeEach)
})
it('redirects to login when not authenticated', async () => {
// Mock authentication state
jest.spyOn(auth, 'isAuthenticated').mockReturnValue(false)
await router.push('/admin')
expect(router.currentRoute.value.name).toBe('Login')
expect(router.currentRoute.value.query.redirect).toBe('/admin')
})
it('allows access when authenticated', async () => {
jest.spyOn(auth, 'isAuthenticated').mockReturnValue(true)
await router.push('/admin')
expect(router.currentRoute.value.path).toBe('/admin')
})
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Integration Testing
javascript
// Cypress example
describe('Navigation Guards', () => {
it('redirects unauthenticated users to login', () => {
cy.visit('/admin')
cy.url().should('include', '/login')
cy.contains('Please log in')
})
it('allows authenticated users to access protected routes', () => {
cy.login('user@example.com', 'password')
cy.visit('/admin')
cy.url().should('include', '/admin')
cy.contains('Admin Panel')
})
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Common Pitfalls
1. Infinite Redirects
javascript
// ❌ Bad: Can cause infinite redirect
router.beforeEach((to, from) => {
if (!isAuthenticated()) {
return { name: 'Login' } // Always redirects, even from Login page
}
})
// ✅ Good: Check current route
router.beforeEach((to, from) => {
if (!isAuthenticated() && to.name !== 'Login') {
return { name: 'Login' }
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
2. Async Guard Issues
javascript
// ❌ Bad: Not handling async properly
router.beforeEach((to, from) => {
checkAuth().then(isAuth => {
if (!isAuth) return { name: 'Login' }
})
// Guard returns undefined immediately
})
// ✅ Good: Proper async handling
router.beforeEach(async (to, from) => {
const isAuth = await checkAuth()
if (!isAuth) return { name: 'Login' }
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
3. Memory Leaks
javascript
// ❌ Bad: Creating new listeners on each guard
router.beforeEach((to, from) => {
window.addEventListener('beforeunload', handleUnload)
})
// ✅ Good: Proper cleanup
router.beforeEach((to, from) => {
// Clean up previous listeners
window.removeEventListener('beforeunload', handleUnload)
if (to.meta.requiresUnloadWarning) {
window.addEventListener('beforeunload', handleUnload)
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Next Steps
- Learn about Route Meta Fields for storing route metadata
- Explore Data Fetching strategies
- Understand Navigation Failures handling