Vue.js: Vue Router
Last updated: 2026-10-20
Vue Router is Vue's official routing manager — it gives your SPA (single-page application) multiple "pages", where each URL maps to a component. Vue Router 4 is the release built for Vue 3 and fully supports the Composition API.
At the heart of SPA routing is the URL ↔ component mapping. Mastering Vue Router 4 is a prerequisite for building any non-trivial Vue application.
1. What You'll Learn
- Initializing with createRouter + createWebHistory
- 5 kinds of routes: static, dynamic, nested, named, catch-all
- Programmatic navigation (router.push / replace / go)
- Route lazy loading (dynamic import)
- Route parameters (params / query)
- Route guards (beforeEach / beforeEnter)
- 5 real-world scenarios
2. The "404 Nightmare" of a SPA
(1) Pain Point: 5 Pages, 5 Copies of the Same Code
Alice's admin dashboard has 5 pages (Home/Products/Orders/Users/Settings):
Four problems:
- The URL never changes: a refresh loses the current page
- Links cannot be shared: you cannot copy the URL to a colleague
- No browser back/forward: a poor user experience
- Bad SEO: search engines cannot make sense of it
(2) The Vue Router 4 Solution: 5 Route Lines + 5 Components
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: () => import('@/views/HomePage.vue') },
{ path: '/products', component: () => import('@/views/ProductsPage.vue') },
{ path: '/orders', component: () => import('@/views/OrdersPage.vue') },
{ path: '/users', component: () => import('@/views/UsersPage.vue') },
{ path: '/settings', component: () => import('@/views/SettingsPage.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
5 route lines + 1 router-view = a complete SPA. The URL stays in sync automatically, and browser back/forward just works.
(3) Benefits
After adopting Vue Router:
- URL sync: automatic, and preserved across refreshes
- Shareable: every page has its own URL
- Browser history: back/forward work normally
- SEO-friendly: SSR support (Nuxt 3)
- Lines of code: 5 v-if branches → 5 lines in a routes array (-80%)
3. Router Initialization
(1) Complete Initialization Code
// src/router/index.js
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
// 1. Define the routes (lazy loaded)
const routes = [
{ path: '/', component: () => import('@/views/HomePage.vue') },
{ path: '/products', component: () => import('@/views/ProductsPage.vue') }
]
// 2. Create the router instance
const router = createRouter({
// history mode: the URL has no # (recommended)
history: createWebHistory(),
// hash mode: the URL contains # (works with older browsers)
// history: createWebHashHistory(),
routes,
// Scroll behavior: scroll back to the top on navigation
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})
export default router
(2) Registering in main.js
(3) The 2 History Modes Compared
| Aspect | createWebHistory | createWebHashHistory |
|---|---|---|
| URL shape | /products |
/#/products |
| SEO | ✅ Friendly | ❌ Unfriendly |
| Server config | Required (try_files) | Not required |
| Browser support | IE10+ | IE8+ |
| Recommendation | ⭐⭐⭐⭐⭐ | ⭐⭐ |
4. 5 Kinds of Route Configuration
(1) Static Routes
const routes = [
{ path: '/', component: () => import('@/views/HomePage.vue') },
{ path: '/about', component: () => import('@/views/AboutPage.vue') },
{ path: '/contact', component: () => import('@/views/ContactPage.vue') }
]
(2) Dynamic Routes (Parameters)
const routes = [
// /products/123
{
path: '/products/:id',
component: () => import('@/views/ProductDetailPage.vue')
},
// /users/456/posts/789
{
path: '/users/:userId/posts/:postId',
component: () => import('@/views/PostPage.vue')
},
// Optional parameter
{
path: '/news/:category?',
component: () => import('@/views/NewsPage.vue')
}
]
<!-- ProductDetailPage.vue reads the parameter -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id) // '123'
</script>
(3) Nested Routes
const routes = [
{
path: '/users',
component: () => import('@/views/UsersPage.vue'),
children: [
{
path: '', // default child route
component: () => import('@/views/UsersListPage.vue')
},
{
path: ':id', // /users/123
component: () => import('@/views/UserDetailPage.vue')
}
]
}
]
(4) Named Routes
const routes = [
{
path: '/products/:id',
name: 'product-detail', // named route
component: () => import('@/views/ProductDetailPage.vue')
}
]
<!-- Navigate by name to avoid hard-coded URLs -->
<router-link :to="{ name: 'product-detail', params: { id: 123 } }">
Product 123
</router-link>
(5) Catch-All 404
const routes = [
{ path: '/', component: () => import('@/views/HomePage.vue') },
// ... other routes
{
path: '/:pathMatch(.*)*', // matches every undefined route
component: () => import('@/views/NotFoundPage.vue')
}
]
▶ Example: A Hand-Written Hash Router (Simulating SPA Routing on the CDN)
▶ Expand runnable example (86 lines)
Output:
A four-link nav bar plus a bordered page area.
Clicking a link changes the hash, and the matching component swaps into the page area;
the active link turns green.
The status line always shows the current route and how many entries the history stack holds, and the two buttons disable themselves at the edges of the history stack.
Click "About" -> hash #/about -> page: ℹ️ About / About us (link turns green)
Click "User 123"-> hash #/user/123-> page: 👤 User 123 / User profile page
Type #/nope -> no match -> page: ❌ 404 Not Found
Status line -> Current route: /about | History stack: 2 entries
vue-router library (which requires Vite).
(1) 📊 5 Navigation Styles at a Glance
| Style | Code | Best For |
|---|---|---|
| Declarative link | <router-link to="/products"> |
The user clicks a link |
| String path | router.push('/products') |
JS-driven navigation |
| Named route | router.push({ name: 'products' }) |
Recommended (renames stay safe) |
| Dynamic params | router.push({ name: 'user', params: { id: 123 } }) |
Detail pages |
| Query params | router.push({ path: '/search', query: { q: 'vue' } }) |
Search / filtering |
Output:
A five-row cheat sheet mapping each navigation style to a code snippet and the situation it fits best.
Use the middle column as copy-paste material and the right column to pick the right style.
Renders one table: 5 rows x 3 columns (Style | Code | Best For).
5. 5 Ways to Navigate
(1) Declarative Navigation (router-link)
(2) Programmatic Navigation (router.push)
import { useRouter } from 'vue-router'
const router = useRouter()
function goToProduct(id) {
router.push(`/products/${id}`) // string
// or
router.push({ name: 'product-detail', params: { id } }) // object
}
function goBack() {
router.back() // go back
}
function goForward() {
router.forward() // go forward
}
function replaceCurrent() {
router.replace('/login') // replace (leaves no history entry)
}
(3) 5 Ways: push vs replace
| Method | Behavior | History |
|---|---|---|
router.push() |
Navigates to a new page | Adds an entry (you can go back) |
router.replace() |
Replaces the current page | Adds no entry (you cannot go back) |
router.go(n) |
Moves n steps forward/back | Walks the history |
router.back() |
Goes back one step | Walks the history |
router.forward() |
Goes forward one step | Walks the history |
(4) Programmatic vs Declarative
| Scenario | Recommendation |
|---|---|
| The user clicks a link | router-link (declarative) |
| JS triggers the navigation | router.push (programmatic) |
| Redirect after a form submit | router.push |
| Redirect after login | router.replace |
| Automatic fallback from a 404 | router.back |
▶ Example: A Tiny Hash Router (the Difference Between push / replace / back)
▶ Try it YourselfReal vue-router cannot run in a browser without a build step, so this example uses
location.hash+hashchangeto hand-write a mini router that focuses on how the three programmatic navigation styles treat browser history.
Output:
On first open (no hash in the URL) the path shows /, the page shows "Home page", and the change counter reads 0.
push('/products'): the address bar becomes #/products, the page becomes "Product list", counter 1.
replace('/search'): the address becomes #/search, the page becomes "Search results", counter 2.
Now call back() (or press the browser back button): you land straight on the original Home page — the #/products entry was overwritten by replace, so replace leaves no history behind.
6. Route Guards (Navigation Guards)
(1) 3 Types of Guards
// 1. Global beforeEach guard (most common)
router.beforeEach((to, from, next) => {
const isLoggedIn = !!localStorage.getItem('token')
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login') // redirect to login
} else {
next() // allow navigation
}
})
// 2. Per-route guard
const routes = [
{
path: '/admin',
component: () => import('@/views/AdminPage.vue'),
beforeEnter: (to, from, next) => {
if (isAdmin()) next()
else next('/403')
}
}
]
// 3. In-component guards
// In-component: beforeRouteEnter / beforeRouteUpdate / beforeRouteLeave
(2) Complete Login Guard Example
// router/index.js
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
const isLoggedIn = !!token
// Pages that require login
if (to.meta.requiresAuth && !isLoggedIn) {
return next({
name: 'login',
query: { redirect: to.fullPath } // come back here after login
})
}
// A logged-in user visiting the login page
if (to.name === 'login' && isLoggedIn) {
return next({ name: 'home' })
}
next()
})
// Route meta configuration
const routes = [
{
path: '/dashboard',
component: () => import('@/views/DashboardPage.vue'),
meta: { requiresAuth: true } // requires login
}
]
7. Putting It Together: Login Guard for the Admin App
▶ Example: A Complete Login Guard (⚠️ Requires a Vite Project)
// router/index.js
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
const isLoggedIn = !!token
if (to.meta.requiresAuth && !isLoggedIn) {
return next({ name: 'login', query: { redirect: to.fullPath } })
}
if (to.name === 'login' && isLoggedIn) {
return next({ name: 'home' })
}
next()
})
// Route meta configuration
const routes = [
{
path: '/dashboard',
component: () => import('@/views/DashboardPage.vue'),
meta: { requiresAuth: true } // requires login
}
]
Output:
No UI of its own — this is the guard that sits in front of every navigation.
With no token, visiting /dashboard redirects to /login?redirect=/dashboard;
after a successful login the redirect sends the user straight back.
A logged-in user who opens /login is bounced to home.
No token + /dashboard -> /login?redirect=%2Fdashboard
token + /login -> /home
token + /dashboard -> allowed, renders DashboardPage
(2) 📊 5 Common Mistakes at a Glance
| Mistake | Symptom | Fix |
|---|---|---|
| Missing router-view | Nothing renders | Add <router-view /> |
| Routes are not lazy loaded | Slow first paint | Use () => import() |
| Guard loops forever | Trapped on one page | Avoid next() re-triggering beforeEach |
| Nested route without an outlet | Child page never shows | Add <router-view /> to the parent |
| Missing 404 route | Errors on unknown URLs | Add path: '/:pathMatch(.*)*' |
Output:
A five-row troubleshooting table listing the routing bugs that account for most "blank page" and "stuck navigation" reports, together with the one-line fix for each.
Renders one table: 5 rows x 3 columns (Mistake | Symptom | Fix).
(3) 📊 5-Way Performance Comparison
| Mode | First paint | Navigation | Best For |
|---|---|---|---|
| Static import | Slow | Fast | Small projects |
| Dynamic import | ⭐⭐⭐⭐⭐ | Slow the first time | Recommended |
| Grouped lazy loading | ⭐⭐⭐⭐⭐ | Slow the first time | Large projects |
| prefetch | ⭐⭐⭐⭐ | Fast | UX-first products |
| SSR | ⭐⭐⭐⭐⭐ | Fast | Nuxt 3 |
Output:
A five-row comparison table.
The takeaway is that lazy loading trades a slower first visit to a route for a much smaller initial bundle;
prefetch and SSR buy both back at the cost of extra setup.
Renders one table: 5 rows x 4 columns (Mode | First paint | Navigation | Best For).
❓ FAQ
useRoute() hook: const route = useRoute(); route.params.id.beforeEach(async (to, from, next) => { await checkAuth(); next() }). Just be careful not to create an infinite loop.children: [], and add <router-view /> in the parent template as the child outlet.meta: { requiresAuth: true } to the route config and read it in a guard through to.meta.requiresAuth. It is commonly used for permissions, page titles, and breadcrumbs.📖 Summary
- Vue Router 4 is the official router for Vue 3
- 5 kinds of routes: static, dynamic, nested, named, catch-all
- 5 navigation styles: router-link, router.push, replace, back, forward
- 3 kinds of guards: global (beforeEach), per-route (beforeEnter), in-component
- Route lazy loading (dynamic import) shrinks the initial bundle
- Prefer createWebHistory; createWebHashHistory is for older browsers
- Nested routes use children plus
<router-view />
📝 Exercises
-
Basic (difficulty ⭐) Build a simple multi-page SPA:
- 3 pages: Home / About / Contact
- Top navigation plus a router-view below
- Configure the routes with
vue-router
-
Intermediate (difficulty ⭐⭐) Build e-commerce product routes:
/productslist page/products/:iddetail page (dynamic params)/products/:id/editedit page- 404 handling
-
Challenge (difficulty ⭐⭐⭐) Build a complete e-commerce admin routing system:
- 10+ pages (home/products/orders/users/settings/login/404, etc.)
- Nested routes (/admin/dashboard, /admin/products, etc.)
- Route lazy loading (fast first paint)
- Login guard (redirect to /login, then back again)
- All 5 navigation styles covered
- Programmatic navigation (buttons that navigate)
- Strongly typed TypeScript routes