Introduction
Building single-page applications means your users never see a white page flash when they click between sections. Everything feels instant and smooth. Vue Router makes this magic happen by intercepting URL changes and swapping out components instead of requesting new pages from the server.
The beauty of Vue Router lies in its simplicity for basic use cases and its power for complex scenarios. You can start with simple page-to-page navigation and gradually add features like authentication guards, lazy loading, and nested layouts as your application grows.
This guide walks through everything you need to know about Vue Router, from the initial setup to advanced patterns that keep your application performant and user-friendly.
Setting Up Vue Router
Getting Vue Router running in your project involves three main steps: installation, route definition, and integration with your Vue app.
Installation
Install Vue Router through your package manager:
npm install vue-router@4
# or
yarn add vue-router@4
💡 Tip: Always specify version 4 when working with Vue 3. Vue Router 3 is for Vue 2 projects and won't work with modern Vue applications.
Defining Routes
Create a dedicated router configuration file. I typically use src/router/index.js
to keep things organized:
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import NotFound from '@/views/NotFound.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
export default router
📌 Note: The catch-all route /:pathMatch(.*)*
handles 404 cases. Always include this to provide a better user experience when someone visits a non-existent page.
Integrating with Your App
Connect the router to your Vue application in main.js
:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
Don't forget to add <router-view />
in your main App.vue template where you want routed components to appear:
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<main>
<router-view />
</main>
</div>
</template>
Dynamic Routing with Parameters
Dynamic routes let you handle patterns like user profiles, product pages, or any content that follows a URL structure with variable parts.
Basic Dynamic Routes
Define dynamic segments using colons:
const routes = [
{ path: '/user/:id', component: UserProfile },
{ path: '/product/:category/:slug', component: ProductDetail }
]
Accessing Route Parameters
In the Composition API (recommended for new projects):
<template>
<div>
<h1>User Profile: {{ userId }}</h1>
<p>Loading user data...</p>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
const userId = computed(() => route.params.id)
// Watch for route changes if the component is reused
watch(() => route.params.id, (newId) => {
// Fetch new user data when ID changes
fetchUserData(newId)
})
</script>
For Options API:
<script>
export default {
computed: {
userId() {
return this.$route.params.id
}
},
watch: {
'$route.params.id': {
handler(newId) {
this.fetchUserData(newId)
},
immediate: true
}
}
}
</script>
⚠️ Warning: When navigating between routes that use the same component (like /user/1
to /user/2
), Vue reuses the component instance. Always watch for parameter changes to update your data accordingly.
Optional Parameters and Wildcards
Handle optional route segments and catch-all patterns:
const routes = [
// Optional parameter
{ path: '/search/:query?', component: SearchResults },
// Multiple parameters with validation
{
path: '/user/:id(\\d+)',
component: UserProfile,
beforeEnter: (to) => {
// Only allow numeric IDs
return /^\d+$/.test(to.params.id)
}
},
// Catch remaining path
{ path: '/files/:pathMatch(.*)', component: FileExplorer }
]
Navigation Guards: Controlling Access
Navigation guards are your security checkpoints. They run before route changes and can prevent navigation, redirect users, or trigger side effects.
Global Guards
Set up application-wide navigation logic:
import { useAuthStore } from '@/stores/auth'
// Runs before every route change
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// Show loading state
const loadingStore = useLoadingStore()
loadingStore.setLoading(true)
// Check if route requires authentication
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
// Check permissions for admin routes
if (to.meta.requiresAdmin && !authStore.isAdmin) {
next({ name: 'Forbidden' })
return
}
next()
})
// Clean up after navigation
router.afterEach(() => {
const loadingStore = useLoadingStore()
loadingStore.setLoading(false)
})
Per-Route Guards
Add guards directly to route definitions:
const routes = [
{
path: '/admin',
component: AdminDashboard,
beforeEnter: (to, from, next) => {
// This only runs when entering this specific route
if (!checkAdminPermissions()) {
next('/unauthorized')
} else {
next()
}
},
meta: { requiresAuth: true, requiresAdmin: true }
}
]
Component Guards
Handle navigation within components:
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// Prevent leaving with unsaved changes
onBeforeRouteLeave((to, from, next) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm('You have unsaved changes. Are you sure you want to leave?')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
})
// Handle route parameter changes
onBeforeRouteUpdate((to, from, next) => {
// Component is reused, update data based on new params
fetchData(to.params.id)
next()
})
</script>
💡 Tip: Use route meta fields to store route-level information like authentication requirements, page titles, or analytics data. This keeps your guard logic clean and declarative.
Lazy Loading and Code Splitting
Lazy loading prevents your initial bundle from becoming massive by loading route components only when needed.
Basic Lazy Loading
Replace direct imports with dynamic imports:
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// Add loading and error components
loading: () => import('@/components/LoadingSpinner.vue'),
error: () => import('@/components/ErrorPage.vue')
}
]
Chunk Grouping
Group related routes into the same chunk to reduce the number of network requests:
const routes = [
{
path: '/admin/users',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Users.vue')
},
{
path: '/admin/settings',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Settings.vue')
},
{
path: '/admin/reports',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Reports.vue')
}
]
⚠️ Warning: Don't lazy load components that users will definitely visit early in their session (like your homepage). The extra network request adds unnecessary delay.
Nested Routes and Layout Management
Nested routes help organize complex applications with multiple levels of navigation.
Setting Up Nested Routes
Create parent-child route relationships:
const routes = [
{
path: '/dashboard',
component: DashboardLayout,
children: [
// Empty path means this will render when /dashboard is matched
{ path: '', component: DashboardHome },
{ path: 'analytics', component: Analytics },
{ path: 'users', component: UserManagement },
{ path: 'settings', component: Settings },
{
path: 'users/:id',
component: UserDetail,
children: [
{ path: '', component: UserOverview },
{ path: 'edit', component: UserEdit },
{ path: 'permissions', component: UserPermissions }
]
}
]
}
]
Layout Component Structure
Your parent component needs a <router-view>
for child routes:
<template>
<div class="dashboard-layout">
<aside class="sidebar">
<nav>
<router-link to="/dashboard">Overview</router-link>
<router-link to="/dashboard/analytics">Analytics</router-link>
<router-link to="/dashboard/users">Users</router-link>
<router-link to="/dashboard/settings">Settings</router-link>
</nav>
</aside>
<main class="content">
<!-- Child routes render here -->
<router-view />
</main>
</div>
</template>
📌 Note: Nested routes inherit the full path from their parents. A child route with path 'users'
under parent path '/dashboard'
becomes /dashboard/users
.
Advanced Navigation Techniques
Programmatic Navigation
Sometimes you need to navigate users based on logic rather than clicks:
// In Composition API
import { useRouter } from 'vue-router'
const router = useRouter()
// Basic navigation
router.push('/about')
// Navigation with parameters
router.push({ name: 'UserProfile', params: { id: 123 } })
// Navigation with query parameters
router.push({ path: '/search', query: { q: 'vue router' } })
// Replace current entry (doesn't add to history)
router.replace('/login')
// Go back/forward
router.go(-1) // Back one page
router.go(1) // Forward one page
Navigation with State
Pass temporary data between routes without exposing it in the URL:
router.push({
name: 'ProductDetail',
params: { id: product.id },
state: { fromCart: true, discountApplied: true }
})
// Access in the destination component
const navigationState = history.state
Handling Navigation Failures
Catch and handle navigation errors gracefully:
async function navigateToProfile(userId) {
try {
await router.push({ name: 'UserProfile', params: { id: userId } })
} catch (error) {
if (error.name === 'NavigationDuplicated') {
// User is already on this page, ignore
return
}
// Handle other navigation errors
console.error('Navigation failed:', error)
showErrorMessage('Failed to load user profile')
}
}
Route Metadata and Configuration
Using Route Meta Fields
Store additional information about routes:
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
roles: ['admin', 'manager'],
title: 'Dashboard',
breadcrumb: 'Dashboard'
}
},
{
path: '/public-page',
component: PublicPage,
meta: {
layout: 'minimal',
analytics: { category: 'marketing' }
}
}
]
Dynamic Route Names and Aliases
Make routes more flexible with aliases and dynamic naming:
const routes = [
{
path: '/user/:id',
name: 'UserProfile',
component: UserProfile,
alias: ['/profile/:id', '/member/:id'], // Multiple ways to reach the same route
props: true // Pass route params as props to component
},
{
path: '/home',
redirect: '/' // Redirect old URLs
}
]
Query Parameters and Hash Handling
Working with Query Parameters
Query parameters persist across navigation and are perfect for filters, search terms, or pagination:
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
const router = useRouter()
const currentPage = computed(() => parseInt(route.query.page) || 1)
const searchTerm = computed(() => route.query.search || '')
function updateSearch(term) {
router.push({
query: { ...route.query, search: term, page: 1 }
})
}
function goToPage(page) {
router.push({
query: { ...route.query, page }
})
}
</script>
Hash-Based Routing
Sometimes you need hash-based routing for deployment constraints:
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes
})
⚠️ Warning: Hash routing means URLs look like yoursite.com/#/about
instead of yoursite.com/about
. Only use this if your hosting setup doesn't support proper history mode configuration.
Performance Optimization Patterns
Smart Lazy Loading Strategy
Not all routes should be lazy loaded. Here's a practical approach:
// Immediately loaded (critical pages)
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
// Lazy loaded (secondary pages)
const routes = [
{ path: '/', component: Home },
{ path: '/login', component: Login },
// Admin section - lazy load everything
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{
path: 'users',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Users.vue')
},
{
path: 'reports',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Reports.vue')
}
]
},
// Feature-based chunking
{
path: '/analytics',
component: () => import(/* webpackChunkName: "analytics" */ '@/views/Analytics.vue')
}
]
Preloading Critical Routes
Preload important routes on user interaction:
<template>
<router-link
to="/dashboard"
@mouseenter="preloadDashboard"
@focus="preloadDashboard"
>
Dashboard
</router-link>
</template>
<script setup>
function preloadDashboard() {
// Preload the component when user hovers/focuses
import('@/views/Dashboard.vue')
}
</script>
Error Handling and Edge Cases
Route Validation
Validate route parameters before allowing navigation:
const routes = [
{
path: '/user/:id(\\d+)', // Only numeric IDs
component: UserProfile,
beforeEnter: (to, from, next) => {
const userId = parseInt(to.params.id)
if (userId < 1 || userId > 999999) {
next({ name: 'NotFound' })
} else {
next()
}
}
}
]
Handling Async Route Components
Deal with loading states and errors in route components:
const routes = [
{
path: '/heavy-page',
component: () => import('@/views/HeavyPage.vue').catch(() => {
// Fallback if component fails to load
return import('@/views/ErrorPage.vue')
})
}
]
Navigation Failure Recovery
Implement retry logic for failed navigation:
async function navigateWithRetry(to, maxRetries = 3) {
let attempts = 0
while (attempts < maxRetries) {
try {
await router.push(to)
return
} catch (error) {
attempts++
if (attempts >= maxRetries) {
// Final fallback
router.push('/')
showNotification('Navigation failed. Redirected to home page.')
} else {
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
}
}
Advanced Router Configuration
Custom History Mode Configuration
Configure the router for different deployment scenarios:
const router = createRouter({
history: createWebHistory('/my-app/'), // Subdirectory deployment
routes,
// Scroll behavior
scrollBehavior(to, from, savedPosition) {
// Return to saved position on back/forward
if (savedPosition) {
return savedPosition
}
// Scroll to anchor if hash exists
if (to.hash) {
return { el: to.hash, behavior: 'smooth' }
}
// Scroll to top for new pages
return { top: 0 }
},
// Link active classes
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active'
})
Route Transitions
Add smooth transitions between route changes:
<template>
<router-view v-slot="{ Component, route }">
<transition
:name="route.meta.transition || 'fade'"
mode="out-in"
>
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.slide-enter-active, .slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
</style>
Common Patterns and Best Practices
Route-Based State Management
Sync route state with your application state:
// In your store
import { useRoute, useRouter } from 'vue-router'
export const useFiltersStore = defineStore('filters', () => {
const route = useRoute()
const router = useRouter()
const filters = computed(() => ({
category: route.query.category || 'all',
price: route.query.price || '',
sort: route.query.sort || 'name'
}))
function updateFilters(newFilters) {
router.push({
query: { ...route.query, ...newFilters }
})
}
return { filters, updateFilters }
})
Loading States
Show appropriate loading states during route transitions:
<template>
<div>
<div v-if="$route.meta.showLoader && isLoading" class="loading-overlay">
<LoadingSpinner />
</div>
<router-view v-slot="{ Component }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
<PageSkeleton />
</template>
</Suspense>
</router-view>
</div>
</template>
SEO and Meta Management
Update page metadata based on routes:
router.beforeEach((to, from, next) => {
// Update page title
document.title = to.meta.title
? `${to.meta.title} - My App`
: 'My App'
// Update meta description
const metaDescription = document.querySelector('meta[name="description"]')
if (metaDescription && to.meta.description) {
metaDescription.setAttribute('content', to.meta.description)
}
next()
})
Router Configuration Reference
Essential Router Options
Option | Purpose | Example | Notes |
---|---|---|---|
history | History mode | createWebHistory() | Use createWebHashHistory() for static hosting |
routes | Route definitions | [{ path: '/', component: Home }] | Required array of route objects |
base | Base URL | '/app/' | For subdirectory deployments |
scrollBehavior | Scroll handling | (to, from, saved) => ({ top: 0 }) | Controls scroll position on navigation |
linkActiveClass | Active link CSS class | 'active' | Applied to matching router-links |
parseQuery /stringifyQuery | Query parsing | Custom functions | Override default query handling |
Route Definition Properties
Property | Type | Purpose | Usage Notes |
---|---|---|---|
path | String | URL pattern | Use :param for dynamic segments |
component | Component/Function | Component to render | Can be direct import or lazy load function |
name | String | Route identifier | Useful for programmatic navigation |
redirect | String/Object/Function | Redirect target | Redirects to another route |
alias | String/Array | Alternative paths | Multiple URLs for same component |
meta | Object | Custom data | Store auth requirements, titles, etc. |
props | Boolean/Object/Function | Pass props to component | true passes route params as props |
beforeEnter | Function | Route-specific guard | Runs only when entering this route |
children | Array | Nested routes | For complex layouts and hierarchies |
Troubleshooting Common Issues
Router View Not Updating
If your router-view isn't updating, check these common issues:
- Missing
key
attribute when you need forced re-renders - Component caching preventing updates
- Missing
<router-view />
in parent component
<!-- Force re-render on route change -->
<router-view :key="$route.fullPath" />
Navigation Guard Infinite Loops
Prevent infinite redirects in navigation guards:
router.beforeEach((to, from, next) => {
// Avoid redirecting to login if already going to login
if (to.name === 'Login') {
next()
return
}
if (!isAuthenticated && to.meta.requiresAuth) {
next({ name: 'Login' })
} else {
next()
}
})
Memory Leaks in Route Components
Clean up subscriptions and listeners:
<script setup>
import { onBeforeUnmount } from 'vue'
let interval
let subscription
onMounted(() => {
interval = setInterval(fetchData, 5000)
subscription = eventBus.subscribe('update', handleUpdate)
})
onBeforeUnmount(() => {
if (interval) clearInterval(interval)
if (subscription) subscription.unsubscribe()
})
</script>
Conclusion
Vue Router transforms how users navigate your application by making every interaction feel instant and smooth. The key to mastering it lies in understanding when to use each feature. Start with basic routing, add dynamic parameters when you need them, implement guards for security, and optimize with lazy loading as your app grows.
The patterns covered here handle 90% of real-world routing scenarios. As you build more complex applications, you'll discover that Vue Router's flexibility lets you solve almost any navigation challenge while keeping your code clean and maintainable.
Remember that good routing feels invisible to users. They should never think about how navigation works because it just works. That's the goal we're aiming for.