7.6: Vue Router
Master state management with Pinia, the official state management library for Vue 3 applications. Learn how to manage global application state, share data between components, and implement centralized state patterns.
1. Introduction to Vue Router
What is Client-Side Routing?
Traditional multi-page apps:
- Each page is a separate HTML file
- Browser makes full page reload on navigation
- Server sends new HTML for each page
Single Page Applications (SPAs):
- One HTML file for entire app
- JavaScript dynamically updates content
- No full page reloads (faster navigation)
- Vue Router handles URL changes
Benefits:
- Faster navigation (no page reloads)
- Smooth transitions between pages
- Better user experience
- Maintains application state
- Works offline (with PWA)
2. Setting Up Vue Router
Installation
When creating a Vue project:
npm create vue@latest
# Select "Yes" for Vue Router
✔ Add Vue Router for Single Page Application development? Yes
Manual installation:
npm install vue-router@4
Basic Setup
Create router configuration:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Register router in main app:
// src/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')
Use router in root component:
<!-- App.vue -->
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div id="app">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
<main>
<RouterView />
</main>
</div>
</template>
3. Basic Routing
Creating Routes
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'about',
component: () => import('@/views/About.vue')
},
{
path: '/contact',
name: 'contact',
component: () => import('@/views/Contact.vue')
},
{
path: '/products',
name: 'products',
component: () => import('@/views/Products.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Lazy loading vs Direct import:
// Direct import - included in initial bundle
import Home from '@/views/Home.vue'
// Lazy loading - separate chunk, loaded on demand
component: () => import('@/views/About.vue')
Navigation with RouterLink
<template>
<nav>
<!-- Basic navigation -->
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<!-- Named routes -->
<RouterLink :to="{ name: 'products' }">Products</RouterLink>
<!-- With parameters -->
<RouterLink :to="{ name: 'user', params: { id: 123 } }">
User Profile
</RouterLink>
<!-- With query parameters -->
<RouterLink :to="{ path: '/search', query: { q: 'vue' } }">
Search
</RouterLink>
<!-- External link (use regular <a> tag) -->
<a href="https://vuejs.org" target="_blank">Vue Docs</a>
</nav>
</template>
<style scoped>
/* Active link styling */
.router-link-active {
font-weight: bold;
color: #42b983;
}
/* Exact active (only when exact match) */
.router-link-exact-active {
border-bottom: 2px solid #42b983;
}
</style>
Programmatic Navigation
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Navigate to route
function goToAbout() {
router.push('/about')
}
// Navigate with name
function goToUser(userId) {
router.push({ name: 'user', params: { id: userId } })
}
// Navigate with query
function searchProducts(query) {
router.push({ path: '/products', query: { search: query } })
}
// Replace current route (doesn't add to history)
function replaceRoute() {
router.replace('/about')
}
// Go back
function goBack() {
router.back()
}
// Go forward
function goForward() {
router.forward()
}
// Go to specific history entry
function goToHistory(delta) {
router.go(delta) // -1 = back, 1 = forward
}
// Get current route info
console.log('Current path:', route.path)
console.log('Route params:', route.params)
console.log('Route query:', route.query)
</script>
<template>
<div>
<button @click="goToAbout">Go to About</button>
<button @click="goBack">Back</button>
<button @click="searchProducts('laptop')">Search Laptops</button>
</div>
</template>
4. Dynamic Routes
Route Parameters
// router/index.js
const routes = [
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/UserProfile.vue')
},
{
path: '/product/:category/:id',
name: 'product',
component: () => import('@/views/ProductDetail.vue')
},
{
// Optional parameter
path: '/search/:query?',
name: 'search',
component: () => import('@/views/Search.vue')
}
]
Accessing parameters:
<!-- views/UserProfile.vue -->
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const user = ref(null)
// Access parameter
const userId = route.params.id
// Fetch user data
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
user.value = await response.json()
}
// Fetch on mount
fetchUser(userId)
// Re-fetch when route changes
watch(
() => route.params.id,
(newId) => {
fetchUser(newId)
}
)
</script>
<template>
<div v-if="user">
<h1>{{ user.name }}</h1>
<p>User ID: {{ route.params.id }}</p>
</div>
</template>
Props Mode
Pass route params as props to component:
// router/index.js
const routes = [
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/UserProfile.vue'),
props: true // Pass params as props
},
{
path: '/product/:id',
name: 'product',
component: () => import('@/views/ProductDetail.vue'),
props: (route) => ({
id: Number(route.params.id), // Convert to number
highlight: route.query.highlight
})
}
]
Component receives props:
<!-- views/UserProfile.vue -->
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
id: {
type: String,
required: true
}
})
const user = ref(null)
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`)
user.value = await response.json()
}
fetchUser(props.id)
watch(() => props.id, (newId) => {
fetchUser(newId)
})
</script>
<template>
<div v-if="user">
<h1>{{ user.name }}</h1>
</div>
</template>
5. Nested Routes
Creating Nested Routes
// router/index.js
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
children: [
{
path: '', // Default child route
name: 'dashboard-home',
component: () => import('@/views/dashboard/Home.vue')
},
{
path: 'profile',
name: 'dashboard-profile',
component: () => import('@/views/dashboard/Profile.vue')
},
{
path: 'settings',
name: 'dashboard-settings',
component: () => import('@/views/dashboard/Settings.vue')
},
{
path: 'posts/:id',
name: 'dashboard-post',
component: () => import('@/views/dashboard/PostDetail.vue')
}
]
}
]
Parent component with nested RouterView:
<!-- views/Dashboard.vue -->
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div class="dashboard">
<aside class="sidebar">
<h2>Dashboard</h2>
<nav>
<RouterLink to="/dashboard">Home</RouterLink>
<RouterLink to="/dashboard/profile">Profile</RouterLink>
<RouterLink to="/dashboard/settings">Settings</RouterLink>
</nav>
</aside>
<main class="content">
<!-- Child routes render here -->
<RouterView />
</main>
</div>
</template>
<style scoped>
.dashboard {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 250px;
background: #f5f5f5;
padding: 1rem;
}
.content {
flex: 1;
padding: 2rem;
}
nav a {
display: block;
padding: 0.5rem 1rem;
margin-bottom: 0.5rem;
border-radius: 4px;
text-decoration: none;
color: #333;
}
nav a.router-link-active {
background: #42b983;
color: white;
}
</style>
6. Navigation Guards
Global Guards
// router/index.js
const router = createRouter({
history: createWebHistory(),
routes
})
// Global before guard (runs before every navigation)
router.beforeEach((to, from, next) => {
console.log('Navigating to:', to.path)
console.log('Coming from:', from.path)
// Check authentication
const isAuthenticated = localStorage.getItem('token')
if (to.meta.requiresAuth && !isAuthenticated) {
// Redirect to login
next({ name: 'login' })
} else {
// Allow navigation
next()
}
})
// Global after guard (runs after navigation is confirmed)
router.afterEach((to, from) => {
// Update page title
document.title = to.meta.title || 'My App'
// Analytics tracking
trackPageView(to.path)
})
export default router
Per-Route Guards
const routes = [
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
beforeEnter: (to, from, next) => {
const user = getUserFromToken()
if (!user || !user.isAdmin) {
next({ name: 'home' })
} else {
next()
}
}
}
]
In-Component Guards
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
const hasUnsavedChanges = ref(false)
// Confirm before leaving if there are unsaved changes
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm('You have unsaved changes. Leave anyway?')
if (!answer) return false
}
})
// React to route parameter changes
onBeforeRouteUpdate((to, from) => {
// Fetch new data when route params change
fetchData(to.params.id)
})
</script>
7. Route Meta Fields
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: {
title: 'Home Page',
requiresAuth: false,
layout: 'default'
}
},
{
path: '/admin',
name: 'admin',
component: Admin,
meta: {
title: 'Admin Panel',
requiresAuth: true,
requiresAdmin: true,
layout: 'admin'
}
},
{
path: '/profile',
name: 'profile',
component: Profile,
meta: {
title: 'My Profile',
requiresAuth: true,
breadcrumb: 'Profile'
}
}
]
// Use meta in navigation guard
router.beforeEach((to, from, next) => {
// Set page title
document.title = to.meta.title || 'App'
// Check authentication
if (to.meta.requiresAuth) {
const isAuthenticated = checkAuth()
if (!isAuthenticated) {
next({ name: 'login' })
return
}
}
// Check admin access
if (to.meta.requiresAdmin) {
const isAdmin = checkAdminStatus()
if (!isAdmin) {
next({ name: 'home' })
return
}
}
next()
})
8. Scroll Behavior
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// Saved position (when using back/forward buttons)
if (savedPosition) {
return savedPosition
}
// Scroll to anchor
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
}
// Always scroll to top
return { top: 0 }
}
})
9. Complete Example: Blog Application
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue'),
meta: { title: 'Home' }
},
{
path: '/blog',
component: () => import('@/views/Blog.vue'),
meta: { title: 'Blog' },
children: [
{
path: '',
name: 'blog-list',
component: () => import('@/views/blog/PostList.vue')
},
{
path: ':slug',
name: 'blog-post',
component: () => import('@/views/blog/PostDetail.vue'),
props: true
},
{
path: 'category/:category',
name: 'blog-category',
component: () => import('@/views/blog/Category.vue'),
props: true
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue'),
meta: { title: 'Login', layout: 'auth' }
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true, title: 'Dashboard' },
children: [
{
path: '',
name: 'dashboard-home',
component: () => import('@/views/dashboard/Home.vue')
},
{
path: 'posts',
name: 'dashboard-posts',
component: () => import('@/views/dashboard/Posts.vue')
},
{
path: 'posts/new',
name: 'dashboard-new-post',
component: () => import('@/views/dashboard/NewPost.vue')
},
{
path: 'posts/:id/edit',
name: 'dashboard-edit-post',
component: () => import('@/views/dashboard/EditPost.vue'),
props: true
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFound.vue'),
meta: { title: '404 Not Found' }
}
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// Navigation guards
router.beforeEach((to, from, next) => {
// Update title
document.title = to.meta.title || 'My Blog'
// Check auth
if (to.meta.requiresAuth) {
const isAuthenticated = localStorage.getItem('token')
if (!isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } })
return
}
}
next()
})
export default router
10. Practical Exercises
Exercise 5.6.1: Multi-Page Portfolio
Create a portfolio site with:
- Home, About, Projects, Contact pages
- Dynamic project detail pages
- Smooth page transitions
- Active link highlighting
Exercise 5.6.2: E-commerce Navigation
Build product browsing:
- Product list by category
- Product detail pages
- Search with query parameters
- Breadcrumb navigation
Exercise 5.6.3: Protected Routes
Implement authentication:
- Login/logout functionality
- Protected admin routes
- Redirect to login if not authenticated
- Redirect to intended page after login
Exercise 5.6.4: Nested Dashboard
Create admin dashboard:
- Sidebar navigation
- Nested routes (users, posts, settings)
- Route guards for admin-only access
- Breadcrumbs showing current location
11. Knowledge Check
Question 1: What's the difference between push and replace?
Show answer
`push` adds a new entry to browser history (can go back). `replace` replaces current entry (can't go back to replaced route). Use `replace` for redirects after login/logout.Question 2: When should you use lazy loading for routes?
Show answer
Use lazy loading for routes that aren't immediately needed (like admin pages, less-visited pages). This reduces initial bundle size and improves initial load time.Question 3: What are navigation guards used for?
Show answer
Navigation guards control route access, check authentication, confirm leaving unsaved changes, set page titles, track analytics, and handle redirects.Question 4: How do nested routes work?
Show answer
Nested routes render inside parent route's `12. Key Takeaways
- Vue Router enables SPAs with client-side navigation
- RouterLink for declarative navigation
- useRouter/useRoute for programmatic navigation
- Dynamic routes use parameters (
) for flexible URLs - Nested routes create hierarchical page structures
- Navigation guards control route access and behavior
- Lazy loading improves performance
- Meta fields store route-specific information
- Props mode passes route params as component props
- Scroll behavior customizes scroll position on navigation
13. Further Resources
Official Documentation:
Next Steps
Excellent! You now know how to build multi-page Vue applications with Vue Router.
In Lesson 5.7: State Management with Pinia, you'll learn how to manage global application state across components.