On this page
Vue Router 4: routes, navigation guards, and lazy loading
Vue Router overview
Vue Router is the official routing library for Vue.js. It integrates deeply with Vue's reactivity system: the current route is a reactive object, navigation triggers component re-renders, and route metadata flows through your application just like any other reactive data.
Vue Router 4 (for Vue 3) supports:
- File-based or code-based route definitions
- Nested routes with
<RouterView>outlets - Dynamic segments and catch-all routes
- History API and hash-based navigation
- Navigation guards (global, per-route, in-component)
- Lazy-loaded route components
- Named routes and named views
- Route meta fields for guards and title management
Installation and setup
npm install vue-routerVue Router is included by default when you choose it during npm create vue@latest.
Defining routes
Routes are defined as an array of RouteRecordRaw objects:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(), // uses the HTML5 History API
routes: [
{
path: '/',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/users/:id', // dynamic segment
component: () => import('@/views/UserView.vue'),
},
],
})History modes
| Mode | URL format | Setup required |
|---|---|---|
createWebHistory() |
/users/1 |
Server must serve index.html for all paths |
createWebHashHistory() |
/#/users/1 |
No server config needed |
createMemoryHistory() |
(none) | For SSR and testing |
Dynamic route segments
Use :paramName to match variable segments. Multiple params are allowed:
{ path: '/users/:userId/posts/:postId', component: PostView }Access params in the component:
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.userId) // string
console.log(route.params.postId) // stringProp passing: Set props: true on the route to receive params as component props — cleaner than accessing useRoute() inside the component.
{ path: '/users/:id', component: UserView, props: true }
// UserView receives id as a propNested routes
Nested routes render inside the parent's <RouterView>:
{
path: '/settings',
component: SettingsLayout,
children: [
{ path: '', component: SettingsGeneral }, // /settings
{ path: 'profile', component: SettingsProfile }, // /settings/profile
{ path: 'security', component: SettingsSecurity }, // /settings/security
],
}SettingsLayout.vue must contain a <RouterView /> to render the active child.
RouterLink and RouterView
<RouterLink> renders an <a> tag with proper handling for the Vue Router history mode:
<!-- Basic link -->
<RouterLink to="/about">About</RouterLink>
<!-- Named route with params -->
<RouterLink :to="{ name: 'UserDetail', params: { id: user.id } }">
{{ user.name }}
</RouterLink>
<!-- With query string -->
<RouterLink :to="{ path: '/search', query: { q: 'vue' } }">Search</RouterLink>Active classes are applied automatically:
router-link-active— route is a partial match (parent route)router-link-exact-active— route is an exact match
<RouterView> is the outlet where the matched component renders. Use the slot to add transitions:
<RouterView v-slot="{ Component }">
<Transition name="slide">
<component :is="Component" :key="$route.path" />
</Transition>
</RouterView>Programmatic navigation
Use useRouter() for imperative navigation:
import { useRouter } from 'vue-router'
const router = useRouter()
// Navigate to a path
router.push('/dashboard')
// Named route
router.push({ name: 'UserDetail', params: { id: 42 } })
// With query
router.push({ path: '/search', query: { q: 'vue' } })
// Replace (no history entry)
router.replace('/login')
// Go back / forward
router.go(-1)
router.back()
router.forward()Navigation guards
Guards let you control navigation — redirect unauthenticated users, confirm unsaved changes, or load data before displaying a route.
Global guards
// Before each navigation
router.beforeEach(async (to, from) => {
// Returning false cancels navigation
// Returning a route object redirects
// Returning nothing (or true) allows navigation
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
// After each navigation (no next() — cannot cancel)
router.afterEach((to, from, failure) => {
if (!failure) trackPageView(to.fullPath)
})Per-route guards
{
path: '/admin',
component: AdminView,
beforeEnter: [(to) => {
if (!isAdmin()) return { name: 'Forbidden' }
}],
}In-component guards with the Composition API
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// Runs before leaving this route
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const confirmed = window.confirm('You have unsaved changes. Leave anyway?')
if (!confirmed) return false
}
})
// Runs when params change but component is reused (e.g., /users/1 → /users/2)
onBeforeRouteUpdate(async (to) => {
await loadUser(to.params.id as string)
})Route meta fields
Add arbitrary metadata to routes via the meta field:
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
roles?: string[]
}
}
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true, title: 'Dashboard', roles: ['admin'] },
},
]Access meta in guards, afterEach, or inside components via route.meta.
Lazy loading and code splitting
All route components should be lazy loaded:
// Automatic code splitting — Vite creates a separate chunk per route
component: () => import('@/views/HeavyView.vue')
// Named chunks (grouped in one file)
component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminView.vue')For routes that are almost always visited (like Home), you can prefetch them:
component: () => import(/* webpackPrefetch: true */ '@/views/HomeView.vue')Scroll behavior
Control the scroll position after navigation:
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// Restore scroll position when using browser back/forward
if (savedPosition) return savedPosition
// Scroll to anchor links
if (to.hash) return { el: to.hash, behavior: 'smooth' }
// Always scroll to top
return { top: 0 }
},
})Practice
- Protected route: Create a
/profileroute that requires authentication. Implement a globalbeforeEachguard that redirects unauthenticated users to/loginand stores the intended destination in aredirectquery param. After login, redirect to the stored destination. - Nested routes: Build a
/settingslayout with three child routes:general,notifications, andsecurity. Each child should be a separate view component loaded lazily. - Route transitions: Wrap
<RouterView>with a<Transition>component to add a fade animation when navigating between routes.
In the next lesson, we will explore Pinia — the official state management library for Vue 3 — with stores, getters, and actions.
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/views/HomeView.vue'),
meta: { title: 'Home' },
},
{
path: '/about',
component: () => import('@/views/AboutView.vue'),
meta: { title: 'About' },
},
{
path: '/users',
component: () => import('@/views/UsersView.vue'),
meta: { requiresAuth: true, title: 'Users' },
children: [
{
path: ':id',
component: () => import('@/views/UserDetailView.vue'),
props: true, // route.params.id is passed as a prop
meta: { title: 'User Detail' },
},
],
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFoundView.vue'),
},
]
export const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition ?? { top: 0 }
},
})
// Navigation guard — runs before each route
router.beforeEach(async (to) => {
const auth = useAuthStore()
// Update document title from route meta
document.title = String(to.meta.title ?? 'My App')
// Redirect unauthenticated users
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
Sign in to track your progress