Vue Router Essentials: Route Mapping, Nested Views, and Browser History Strategies

Client-side routing in Vue relies on mapping URL paths to component trees. A route record defines one such mapping—a single path paired with a single component. A collection of these records forms the route table, which the router instance consumes to decide which components to mount or destroy as the location changes.

In the template, <router-link> renders an accessible anchor for navigation. Its to attribute declares the destination path:

<router-link to="/dashboard">Dashboard</router-link>

The companion element <router-view> serves as a layout placeholder; the router will render the matched component exactly where this tag appears.

Configuring Routes

A route table is an array of record objects. Each record needs at least a path and a component:

const routeTable = [
  { path: '/dashboard', component: DashboardView },
  { path: '/profile', component: ProfileView }
]

Create the router manager by passing the table to the VueRouter constructor:

const appRouter = new VueRouter({
  routes: routeTable
})

Finally, inject the router into the root Vue instance so the entire application can react to navigation events:

new Vue({
  router: appRouter,
  render: h => h(AppShell)
}).$mount('#app')

When a user clicks a <router-link>, Vue Router searches the table for a record whose path matches the to value. The associated component is then instantiated inside the nearest <router-view>.

Walkthrough: Two-View Application

Start with two simple view components. DashboardView.vue:

<template>
  <div class="panel">
    <h2>Dashboard</h2>
    <p>{{ headline }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      headline: 'Welcome to the main dashboard'
    }
  }
}
</script>

ProfileView.vue:

<template>
  <div class="panel">
    <h2>Profile</h2>
    <p>{{ description }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      description: 'Manage your personal information here'
    }
  }
}
</script>

In App.vue, provide navigation targets and a render outlet:

<template>
  <div id="app">
    <nav>
      <router-link to="/dashboard">Dashboard</router-link>
      <router-link to="/profile">Profile</router-link>
    </nav>
    <main>
      <router-view />
    </main>
  </div>
</template>

Assemble the mapping in router.js:

import Vue from 'vue'
import VueRouter from 'vue-router'
import DashboardView from './DashboardView.vue'
import ProfileView from './ProfileView.vue'

Vue.use(VueRouter)

const routeTable = [
  { path: '/dashboard', component: DashboardView },
  { path: '/profile', component: ProfileView }
]

export default new VueRouter({ routes: routeTable })

And wire it into main.js:

import Vue from 'vue'
import AppShell from './App.vue'
import appRouter from './router'

new Vue({
  el: '#app',
  router: appRouter,
  render: h => h(AppShell)
})

Redirecting the Root Path

Landing on / initially produces an empty outlet because no record matches the root path. A redirect resolves this by sending the browser to a valid record immediately:

const routeTable = [
  { path: '/dashboard', component: DashboardView },
  { path: '/profile', component: ProfileView },
  { path: '/', redirect: '/dashboard' }
]

Active Link Styling

Vue Router adds a default class—router-link-active—to the anchor that matches the current route. Target it with CSS:

a.router-link-active {
  color: crimson;
}

Inactive links can be styled by attaching an ordinary class directly to the <router-link> element.

Dynamic Segments

Static paths are not always sufficient. Consider a TaskDetail view whose content depends on an identifier. Declare a dynamic segment with a colon:

{ path: '/task/:taskId', component: TaskDetail }

Links pass concrete values:

<router-link to="/task/501">Task 501</router-link>
<router-link to="/task/937">Task 937</router-link>

Inside TaskDetail.vue, expose the parameter through a computed property:

<template>
  <div>
    <h3>Task Details</h3>
    <p>Currently viewing task {{ activeTaskId }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    activeTaskId() {
      return this.$route.params.taskId
    }
  }
}
</script>

Because Vue reuses the same component instance when only the parameter changes, lifecycle hooks do not refire. To react to swaps, watch $route:

export default {
  data() {
    return {
      currentId: null
    }
  },
  watch: {
    $route(updatedRoute) {
      this.currentId = updatedRoute.params.taskId
    }
  }
}

Nested Routes

When a view hosts its own sub-navigation, use nested routes. Suppose Workspace contains three tabs: Projects, Reports, and Settings.

Workspace.vue:

<template>
  <div>
    <h2>Workspace</h2>
    <nav>
      <router-link to="/workspace/projects">Projects</router-link>
      <router-link to="/workspace/reports">Reports</router-link>
      <router-link to="/workspace/settings">Settings</router-link>
    </nav>
    <router-view />
  </div>
</template>

The route configuration uses the children array:

const routeTable = [
  {
    path: '/workspace',
    component: Workspace,
    children: [
      { path: 'projects', component: ProjectsTab },
      { path: 'reports', component: ReportsTab },
      { path: 'settings', component: SettingsTab },
      { path: '', component: ProjectsTab }
    ]
  }
]

The empty child path renders a default sub-view when the parent path is matched exactly.

Named Routes

Assigning a name to a record decouples templates from literal paths:

{
  path: '/task/:taskId',
  name: 'taskDetail',
  component: TaskDetail
}

Reference it with an object in the to prop:

<router-link :to="{ name: 'taskDetail', params: { taskId: 501 } }">
  Open Task 501
</router-link>

Programmatic Navigation

Imperative routing is available through the injected router instance:

this.$router.push('/workspace/projects')

Location objects are also accepted:

this.$router.push({ name: 'taskDetail', params: { taskId: 937 } })

Hash Mode vs. History Mode

Vue Router supports two strategies for updating the URL without requesting a new page from the server. Configure the desired behavior through the mode option:

const appRouter = new VueRouter({
  mode: 'history', // defaults to 'hash' when omitted
  routes: routeTable
})

Hash mode inserts a # fragment between the host and the application path, producing URLs such as https://app.com/#/dashboard. The fragment identifier is never sent to the server, which means static hosts and legacy servers require no additional configuration to support a single-page application.

History mode removes the hash by leveraging the HTML5 History API—pushState and replaceState—to generate clean URLs like https://app.com/dashboard. This approach requires server-side collaboration: when a visitor refreshes the browser or navigates directly to a deep link, the server must return the same index.html shell for every path that the application handles. Otherwise, the server responds with a 404 error because no physical file exists at /dashboard.

Comparing the two:

  • Hash mode works out of the box everywhere but can interfere with anchor scrolling and may appear less polished in public-facing URLs.
  • History mode supports arbitrary path shapes and allows additional state objects to be attached to history entries. It is the preferred choice for modern deployments, provided the hosting environment is configured with a fallback rewrite rule—such as try_files in Nginx or a catch-all directive in Apache—that delegates unknown paths to the application entry point.

Tags: Vue Router Vue.js Client-Side Routing Hash Mode History Mode

Posted on Mon, 14 Sep 2026 16:51:59 +0000 by erwt