Vue Router Complete Guide — SPA Routing & Navigation
Vue Router is the official routing library for Vue.js that maps URL paths to components, enabling single-page applications with multiple views and no page reloads.
What You’ll Learn
- Set up Vue Router with clean URLs
- Define routes with parameters, query strings, and nested routes
- Navigate programmatically and with RouterLink
- Protect routes with global, per-route, and in-component guards
- Lazy-load routes for faster initial page load
- Handle 404 pages and scroll behavior
- Build a complete multi-page SPA
Why Vue Router Matters
Traditional websites reload the entire page on every navigation — slow and jarring. Single-page applications (SPAs) load once and swap views instantly. Vue Router makes this seamless without browser refreshes.
At DodaTech, Vue Router powers the Durga Antivirus Pro dashboard where users navigate between threat analysis, scan history, and settings panels without page reloads. Similarly, DodaZIP’s file explorer uses nested routes to browse folders and file details. The instant navigation creates a native-app feel.
Your Learning Path
flowchart LR
A[Vue Components] --> B[Vue Router]
B --> C[Vue Advanced]
B:::current
classDef current fill:#42b883,color:#fff,stroke:#3aa876,stroke-width:2px
The GPS Analogy — How Vue Router Works
Think of Vue Router as a GPS navigation system for your app:
- Routes = destination addresses (URL paths like
/about,/products) - Router = the GPS that reads the address and displays the right view
- RouterLink = clicking a destination on the screen (“Navigate to…”)
- RouterView = the screen that shows the current destination
- Navigation guards = traffic checkpoints (“ID required to pass”)
Without a router, changing pages means loading an entirely new HTML file from the server — like driving to a new city and getting a new car. With Vue Router, you stay in the same app and just swap the view — like changing the channel on a TV. The TV stays on, only the content changes.
Setup
# Install Vue Router v4 (compatible with Vue 3)
npm install vue-router@4// src/router/index.js — route configuration file
import { createRouter, createWebHistory } from "vue-router";
import HomePage from "../views/HomePage.vue";
import AboutPage from "../views/AboutPage.vue";
// Step 1: Define your routes — an array of path → component mappings.
// Each route object has:
// - path: the URL pattern (string)
// - name: optional identifier for the route
// - component: the Vue component to render when this path matches
const routes = [
{ path: "/", name: "home", component: HomePage },
{ path: "/about", name: "about", component: AboutPage }
];
// Step 2: Create the router instance
const router = createRouter({
// createWebHistory() gives clean URLs like /about (no #).
// Alternative: createWebHashHistory() uses /#/about URLs.
// Hash history works without server config — useful for demos.
history: createWebHistory(),
routes
});
export default router;// src/main.js — register router with the Vue app
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router); // Makes $router and $route available in all components
app.mount("#app");Template Usage
<template>
<nav>
<!-- RouterLink renders as <a> but intercepts clicks — no page reload.
Compare to <a href="/about"> which would trigger a full page load. -->
<RouterLink to="/">Home</RouterLink>
<!-- Can use path string or named route -->
<RouterLink to="/about">About</RouterLink>
<RouterLink :to="{ name: 'about' }">About (named)</RouterLink>
</nav>
<!-- RouterView is where the matched component renders.
Think of it as a placeholder that Vue Router swaps in/out
when the URL changes. It's like a TV screen that changes
channels based on what path you're at. -->
<RouterView />
</template>RouterLink vs regular <a> tag:
| Feature | <a href="/about"> | <RouterLink to="/about"> |
|---|---|---|
| Page reload? | Yes | No |
| SPA-friendly? | No | Yes |
| Active class | Manual | Automatic (router-link-active) |
Route Parameters — Dynamic Routes
Real apps have dynamic URLs: /products/1, /products/2, /users/alice. You don’t write a separate route for each one — you use route parameters with :param placeholders:
const routes = [
{
path: "/products/:id", // :id captures anything after /products/
name: "product-detail",
component: ProductDetail,
props: true // Pass params as component props (recommended)
}
];<!-- ProductDetail.vue — accessing params via props -->
<template>
<div>
<!-- `id` comes from route params, passed as a prop.
With props: true, the component doesn't need to know
about the router — it just receives `id` like any other prop. -->
<h2>Product {{ id }}</h2>
<button @click="goNext">Next Product</button>
</div>
</template>
<script>
export default {
props: ["id"], // Receives route param as prop (thanks to props: true)
methods: {
goNext() {
// Programmatic navigation — like typing a URL in the address bar.
// $router.push adds a new entry to the browser's history stack.
this.$router.push({
name: "product-detail",
params: { id: Number(this.id) + 1 }
});
}
}
};
</script>Why props: true? Without it, you’d use this.$route.params.id — tightly coupling the component to the router. With props: true, the component is reusable outside the router context (e.g., in unit tests or as a child component).
Query Parameters
// Navigate to: /search?q=vue&page=2
this.$router.push({ path: "/search", query: { q: "vue", page: 2 } });
// Read: this.$route.query.q → "vue", this.$route.query.page → "2"
Nested Routes — The Dashboard Pattern
Nested routes render child components inside a parent component — perfect for layouts with sidebars where only a portion of the page changes:
const routes = [
{
path: "/dashboard",
component: DashboardLayout, // Parent layout with sidebar + <RouterView>
children: [
// child paths are RELATIVE to parent — no / prefix needed
{ path: "", name: "dashboard", component: DashboardHome }, // /dashboard
{ path: "analytics", name: "analytics", component: AnalyticsView }, // /dashboard/analytics
{ path: "settings", name: "settings", component: SettingsView } // /dashboard/settings
]
}
];<!-- DashboardLayout.vue — provides the sidebar + RouterView for children -->
<template>
<div class="dashboard-layout">
<aside>
<RouterLink to="/dashboard">Overview</RouterLink>
<RouterLink to="/dashboard/analytics">Analytics</RouterLink>
<RouterLink to="/dashboard/settings">Settings</RouterLink>
</aside>
<main>
<!-- Children render HERE — nested RouterView.
The sidebar stays visible, only this main area changes. -->
<RouterView />
</main>
</div>
</template>Why nested routes? The sidebar persists across all dashboard pages. Only the content area changes. No repeated layout code in every component.
Navigation Guards — Security Checkpoints
Guards protect routes. Think of them as bouncers at a club: “ID, please” before entering.
Global Guard — beforeEach
Runs before EVERY route navigation. Best for auth checks:
// router/index.js
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem("token");
// to — the route we're navigating TO
// from — the route we're navigating FROM
// next — a function we MUST call to proceed
// Check if the route requires authentication.
// to.meta is custom metadata we can attach to any route.
if (to.meta.requiresAuth && !isAuthenticated) {
// Redirect to login, preserving the intended destination
// as a query param so we can redirect back after login.
next({ name: "login", query: { redirect: to.fullPath } });
} else {
next(); // Allow navigation — no next() means the navigation hangs
}
});
router.afterEach((to, from) => {
// Runs after navigation completes — no next() here.
// Perfect for analytics, scroll reset, page title updates.
window.scrollTo(0, 0);
});Per-Route Guard — beforeEnter
const routes = [
{
path: "/admin",
component: AdminPanel,
meta: { requiresAuth: true }, // Custom metadata
beforeEnter: (to, from) => {
// In Vue Router 4, you can RETURN a route location to redirect
if (!localStorage.getItem("token")) {
return { name: "login" };
}
}
}
];In-Component Guards
<script>
export default {
beforeRouteEnter(to, from, next) {
// Cannot access `this` — component not yet created.
// Use next(vm => { ... }) where vm is the component instance.
next(vm => {
vm.loadData();
});
},
beforeRouteUpdate(to, from) {
// Called when route changes but component is reused
// (e.g., navigating from /products/1 to /products/2).
// `this` IS available here — unlike beforeRouteEnter.
this.loadData(to.params.id);
},
beforeRouteLeave(to, from) {
// Warn user about unsaved changes.
// Return false to cancel navigation.
const answer = confirm("Leave without saving?");
if (!answer) return false;
}
};
</script>Global vs per-route vs in-component — when to use which:
| Guard Type | Scope | Use Case |
|---|---|---|
Global (beforeEach) | All routes | Auth checks, analytics, page title |
Per-route (beforeEnter) | One specific route | Admin-only access, feature flags |
| In-component | Component instance | Unsaved changes warning, data preloading |
Lazy Loading — Faster Initial Load
Without lazy loading, all route components are bundled into one massive JS file. Lazy loading splits them into separate chunks loaded on demand:
const routes = [
{
path: "/dashboard",
// Arrow function import — Vue Router loads this file
// ONLY when /dashboard is visited, not on initial page load.
// This reduces the initial bundle size significantly.
component: () => import("../views/DashboardView.vue")
},
{
path: "/products",
component: () => import("../views/ProductList.vue")
}
];Combine with Suspense for loading states:
<RouterView v-slot="{ Component }">
<Suspense>
<!-- The lazy-loaded component renders here -->
<component :is="Component" />
<template #fallback>
<div>Loading page...</div> <!-- Shown while chunk downloads -->
</template>
</Suspense>
</RouterView>Navigation Methods
// Push — adds to history stack (user can click "Back")
this.$router.push("/about");
this.$router.push({ name: "product-detail", params: { id: 3 } });
this.$router.push({ path: "/search", query: { q: "vue" } });
// Replace — replaces current history entry (no "Back" to this page)
this.$router.replace("/login");
// Go — relative to current position
this.$router.go(-1); // Back one step
this.$router.go(1); // Forward one step
// RouterLink props — control link behavior
<RouterLink to="/about" replace /> // replace instead of push
<RouterLink to="/about" active-class="active" /> // custom active CSS class
<RouterLink to="/about" exact-active-class="exact" />Scroll Behavior
Control scroll position when navigating — useful for long pages:
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// If user clicks back/forward, restore exact scroll position
if (savedPosition) {
return savedPosition;
}
// If route has a hash (e.g., /faq#section-2), scroll to that element
if (to.hash) {
return { el: to.hash, behavior: "smooth" };
}
// Default: scroll to top of page
return { top: 0 };
}
});404 Route — Catch-All
Always add a catch-all as the last route:
const routes = [
// ... all other routes go before this
// :pathMatch(.*)* captures any unmatched path
{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFoundPage }
];Without this, unknown paths show a blank page — users have no way back.
Router Error Handling
// router.push may throw on duplicate navigation or guard rejection.
// Always wrap in try/catch for production apps.
try {
await this.$router.push("/products");
} catch (error) {
// NavigationDuplicated is common — not actually an error
if (error.name !== "NavigationDuplicated") {
console.error("Navigation failed:", error);
}
}Common Mistakes
1. Using hash history instead of HTML5 history
createWebHashHistory creates #/about URLs — less clean and breaks SSR. Use createWebHistory with proper server fallback.
2. Not using props: true in route config
Accessing this.$route.params.id couples the component to the router. Use props: true for reusable components you can test independently.
3. Forgetting the catch-all 404 route
Without a catch-all, unknown paths show a blank page. Add { path: '/:pathMatch(.*)*', component: NotFoundPage } as the last route.
4. Not handling router.push errors
router.push throws on duplicate navigations. Wrap in try/catch or configure router.onError.
5. Putting data fetching in navigation guards
Guards handle auth, not data. Fetch data in beforeRouteEnter or mounted() lifecycle hooks instead.
6. Forgetting to call next() in beforeEach
If you forget next(), the navigation hangs indefinitely — the user sees a blank page. Always call next() in every branch.
Practice Questions
1. How do you pass route params as component props?
Set props: true in the route definition. Route params become component props.
2. What’s the difference between router.push and router.replace?
push adds a new history entry (user can go back). replace replaces the current entry (no back navigation).
3. How do you lazy-load routes?
Use arrow function imports: component: () => import('./MyComponent.vue'). Vue loads the chunk only when the route is visited.
4. How do you protect routes from unauthenticated users?
Use a global beforeEach guard that checks auth state and redirects to login if needed.
5. How do you handle 404 pages?
Add a catch-all route { path: '/:pathMatch(.*)*', component: NotFoundPage } as the last route in the array.
Challenge: Build a multi-page SPA with a product listing page, product detail pages (with :id param), a cart page, and auth-protected checkout. Implement lazy loading for all pages and a navigation guard that redirects unauthenticated users to login.
FAQ
Try It Yourself
Copy this into a new HTML file and open it in your browser:
<!DOCTYPE html>
<html>
<head>
<title>Vue Router Demo</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-router@4"></script>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
nav { display: flex; gap: 16px; padding: 12px; background: #42b883; border-radius: 8px; margin-bottom: 20px; }
nav a { color: white; text-decoration: none; font-weight: bold; }
nav a.router-link-active { text-decoration: underline; }
.page { padding: 20px; background: #f9f9f9; border-radius: 8px; }
button { background: #42b883; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div id="app">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/products">Products</RouterLink>
</nav>
<RouterView />
</div>
<script>
const { createApp } = Vue;
const { createRouter, createWebHashHistory, RouterLink, RouterView } = VueRouter;
const Home = { template: '<div class="page"><h2>Home Page</h2><p>Welcome to the Vue Router demo!</p></div>' };
const About = { template: '<div class="page"><h2>About</h2><p>This is a client-side rendered SPA.</p></div>' };
const Products = {
data() { return { items: ["Vue Mug", "JS T-Shirt", "Dev Stickers"] }; },
template: `<div class="page"><h2>Products</h2><ul><li v-for="item in items">{{ item }}</li></ul></div>`
};
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/products", component: Products }
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
const app = createApp({});
app.use(router);
app.mount("#app");
</script>
</body>
</html>What to try: Click the nav links — notice no page reloads. Open your browser console and try router.push('/about'). Add more routes and see how easy it is to extend.
What’s Next
| Resource | Description |
|---|---|
| https://tutorials.dodatech.com/frameworks/vue/vue-advanced/ | Composition API, Pinia, advanced Vue |
| https://tutorials.dodatech.com/frameworks/vue/reference/ | Complete Vue.js API reference |
| Node.js | Learn server-side JS for API backends |
| TypeScript | Add types to your Vue Router projects |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this Vue Router tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro