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


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:

(2) The Vue Router 4 Solution: 5 Route Lines + 5 Components

JS
// 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:


3. Router Initialization

(1) Complete Initialization Code

JS
// 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

JS
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)

JS
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') 
  }
]
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

JS
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

JS
const routes = [
  { 
    path: '/products/:id', 
    name: 'product-detail',  // named route
    component: () => import('@/views/ProductDetailPage.vue') 
  }
]
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

JS
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)
▶ Try it Yourself

Output:

TEXT
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.
TEXT
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
⚠️ This is a teaching demo of how hash routing works. Production projects should use the official 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:

TEXT
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.
TEXT
Renders one table: 5 rows x 3 columns (Style | Code | Best For).

5. 5 Ways to Navigate

(2) Programmatic Navigation (router.push)

JS
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)

Real vue-router cannot run in a browser without a build step, so this example uses location.hash + hashchange to hand-write a mini router that focuses on how the three programmatic navigation styles treat browser history.

▶ Try it Yourself

Output:

TEXT
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

JS
// 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

JS
// 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()
})
JS
// 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)

JS
// 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
  }
]
▶ Try it Yourself

Output:

TEXT
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.
TEXT
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:

TEXT
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.
TEXT
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:

TEXT
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.
TEXT
Renders one table: 5 rows x 4 columns (Mode | First paint | Navigation | Best For).

❓ FAQ

Q createWebHistory or createWebHashHistory?
A createWebHistory (no # in the URL, SEO-friendly) needs server configuration; createWebHashHistory (# in the URL) works with older browsers. Production projects should prefer createWebHistory.
Q How do I read parameters from a dynamic route?
A With the useRoute() hook: const route = useRoute(); route.params.id.
Q What is the difference between router.push and router.replace?
A push adds a history entry (you can go back); replace swaps the current entry (no new history). Use replace after login so the user cannot go back to the login page.
Q Can route guards be async?
A Yes. beforeEach(async (to, from, next) => { await checkAuth(); next() }). Just be careful not to create an infinite loop.
Q How do I implement nested routes?
A Configure the children in the parent route with children: [], and add <router-view /> in the parent template as the child outlet.
Q What changed between Vue Router 3 and Vue Router 4?
A Vue Router 4 supports the Composition API (useRouter/useRoute), has stronger TypeScript inference, and changed the guard signature (to/from/next). Vue 3 projects must use Vue Router 4.
Q How much performance does lazy loading actually buy?
A It can cut first-paint time by 50-80%, because unvisited pages are never loaded. The effect is significant in large projects (10+ pages).
Q How do I use the meta field on a route?
A Add 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


📝 Exercises

  1. 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
  2. Intermediate (difficulty ⭐⭐) Build e-commerce product routes:

    • /products list page
    • /products/:id detail page (dynamic params)
    • /products/:id/edit edit page
    • 404 handling
  3. Challenge (difficulty ⭐⭐⭐) Build a complete e-commerce admin routing system:

    1. 10+ pages (home/products/orders/users/settings/login/404, etc.)
    2. Nested routes (/admin/dashboard, /admin/products, etc.)
    3. Route lazy loading (fast first paint)
    4. Login guard (redirect to /login, then back again)
    5. All 5 navigation styles covered
    6. Programmatic navigation (buttons that navigate)
    7. Strongly typed TypeScript routes
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏