Skip to content
Vue Router Complete Guide — SPA Routing & Navigation

Vue Router Complete Guide — SPA Routing & Navigation

DodaTech Updated Jun 6, 2026 12 min read

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
  
Prerequisites: Complete https://tutorials.dodatech.com/frameworks/vue/vue-components/. You need JavaScript (modules, arrow functions) and basic HTML. Understanding URLs and HTTP helps but isn’t required.

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?YesNo
SPA-friendly?NoYes
Active classManualAutomatic (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 TypeScopeUse Case
Global (beforeEach)All routesAuth checks, analytics, page title
Per-route (beforeEnter)One specific routeAdmin-only access, feature flags
In-componentComponent instanceUnsaved 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

How do I pass props to a routed component?
Set props: true in the route definition. Route params become component props automatically.
What is the difference between router.push and router.replace?
push adds a new entry to the history stack. replace replaces the current entry — the user cannot go back.
How do I lazy-load routes?
Use arrow function imports: component: () => import(’./MyComponent.vue’). Vue Router loads the component only when the route is visited.
How do I protect routes from unauthenticated users?
Use a global beforeEach guard that checks auth state and redirects to login if needed.
How do nested routes work?
A parent route has a children array. The parent component includes a RouterView where child components render.
How do I handle 404 pages?
Add a catch-all route { path: ‘/:pathMatch(.)’, component: NotFoundPage } as the last route in the array.

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

ResourceDescription
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.jsLearn server-side JS for API backends
TypeScriptAdd 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