Vue.js course Β· Module 10: Vue Router

Dynamic Routes

2 min read
In this lesson6

Each NOVA LAB base module has its unique identifier. Similarly, we can create dynamic paths in the application - routes that accept parameters in the URL.

Route Params

A dynamic parameter is defined with a colon : in the path:

1const routes = [
2  {
3    path: '/system/:id',
4    name: 'system-detail',
5    component: SystemDetail
6  },
7  {
8    path: '/crew/:name',
9    name: 'crew-detail',
10    component: CrewDetail
11  },
12  // Multiple parameters
13  {
14    path: '/module/:moduleId/system/:systemId',
15    name: 'module-system',
16    component: ModuleSystem
17  }
18]

Reading Parameters - useRoute()

The useRoute() composable gives access to information about the current route, including parameters:

1<script setup>
2import { useRoute } from 'vue-router'
3
4const route = useRoute()
5
6// Access to parameters
7console.log(route.params.id) // e.g., '42'
8console.log(route.path)      // e.g., '/system/42'
9console.log(route.name)      // e.g., 'system-detail'
10</script>
11
12<template>
13  <div>
14    <h1>System #{{ route.params.id }}</h1>
15  </div>
16</template>
1<template>
2  <div v-for="system in systems" :key="system.id">
3    <!-- String URL -->
4    <RouterLink :to="`/system/${system.id}`">
5      {{ system.name }}
6    </RouterLink>
7
8    <!-- Object notation (recommended) -->
9    <RouterLink :to="{ name: 'system-detail', params: { id: system.id } }">
10      {{ system.name }}
11    </RouterLink>
12  </div>
13</template>

Reactivity to Params Changes

When you change a parameter within the same route, the component is NOT unmounted. Use watch to react to changes:

1<script setup>
2import { ref, watch } from 'vue'
3import { useRoute } from 'vue-router'
4
5const route = useRoute()
6const systemData = ref(null)
7
8watch(
9  () => route.params.id,
10  (newId) => {
11    // Load new system data
12    loadSystem(newId)
13  },
14  { immediate: true }
15)
16</script>

Query Parameters

Query parameters are the part of the URL after the ? character:

1<script setup>
2import { useRoute, useRouter } from 'vue-router'
3
4const route = useRoute()
5const router = useRouter()
6
7// URL: /systems?module=habitat&sort=priority
8console.log(route.query.module) // 'habitat'
9console.log(route.query.sort)   // 'priority'
10
11// Navigation with query
12function filterByModule(module) {
13  router.push({
14    name: 'systems',
15    query: { module, sort: 'priority' }
16  })
17}
18</script>

Optional and Validated Parameters

1const routes = [
2  // Optional parameter with ?
3  { path: '/system/:id?' },
4
5  // Regex - numbers only
6  { path: '/system/:id(\\d+)' },
7
8  // Repeatable parameter
9  { path: '/modules/:ids+' }
10]
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3
4// Simulation of nested routes
5const currentPath = ref('/mission-control')
6const sidebarItems = [
7  { path: '/mission-control', label: 'Overview', name: 'overview' },
8  { path: '/mission-control/systems', label: 'Systems', name: 'systems' },
9  { path: '/mission-control/crew', label: 'Crew', name: 'crew' }
10]
11
12function navigate(path) {
13  currentPath.value = path
14}
15
16const isActive = (path) => currentPath.value === path
17</script>
18
19<template>
20  <div class="nova-lab">
21    <h1>NOVA LAB - Nested Routes</h1>
22
23    <div class="layout">
24      <aside class="sidebar">
25        <h3>Mission Control Center</h3>
26        <nav>
27          <a
28            v-for="item in sidebarItems"
29            :key="item.path"
30            @click.prevent="navigate(item.path)"
31            :class="{ active: isActive(item.path) }"
32            href="#"
33          >
34            {{ item.label }}
35          </a>
36        </nav>
37
38        <div class="config-info">
39          <pre>
40children: [
41  { path: '', component: Overview },
42  { path: 'systems', component: Systems },
43  { path: 'crew', component: Crew }
44]</pre>
45        </div>
46      </aside>
47
48      <main class="content">
49        <div v-if="currentPath === '/mission-control'" class="child-page">
50          <h2>Mission Overview</h2>
51          <p>Default view (path: '')</p>
52          <div class="stats">
53            <div class="stat">
54              <span class="label">Mission status</span>
55              <span class="value ok">ACTIVE</span>
56            </div>
57            <div class="stat">
58              <span class="label">Mission day</span>
59              <span class="value">247</span>
60            </div>
61            <div class="stat">
62              <span class="label">Crew</span>
63              <span class="value">12 people</span>
64            </div>
65          </div>
66        </div>
67
68        <div v-else-if="currentPath === '/mission-control/systems'" class="child-page">
69          <h2>Base Systems</h2>
70          <p>Child route: path: 'systems'</p>
71          <ul class="systems-list">
72            <li>Oxygen Generator - ONLINE</li>
73            <li>Water Recycler - ONLINE</li>
74            <li>Power Grid - WARNING</li>
75            <li>Communications - ONLINE</li>
76          </ul>
77        </div>
78
79        <div v-else-if="currentPath === '/mission-control/crew'" class="child-page">
80          <h2>Crew</h2>
81          <p>Child route: path: 'crew'</p>
82          <ul class="crew-list">
83            <li>Dr. Nova - Commander</li>
84            <li>Eng. Kowalski - Systems</li>
85            <li>Dr. Chen - Science</li>
86          </ul>
87        </div>
88      </main>
89    </div>
90  </div>
91</template>
92
93<style scoped>
94.nova-lab {
95  background: #0a0e27;
96  color: #00ff88;
97  min-height: 100vh;
98  padding: 20px;
99  font-family: monospace;
100}
101h1 { color: #00ffff; margin-bottom: 15px; }
102h2 { color: #00b4d8; }
103h3 { color: #00ffff; margin-bottom: 10px; }
104p { color: #c0c0c0; }
105.layout {
106  display: grid;
107  grid-template-columns: 220px 1fr;
108  gap: 15px;
109}
110.sidebar {
111  background: rgba(0, 180, 216, 0.05);
112  border: 1px solid #1a1e37;
113  padding: 15px;
114}
115.sidebar nav { display: flex; flex-direction: column; gap: 5px; }
116.sidebar a {
117  color: #00ff88;
118  text-decoration: none;
119  padding: 8px;
120  border-left: 2px solid transparent;
121  cursor: pointer;
122}
123.sidebar a.active {
124  border-left-color: #00ffff;
125  color: #00ffff;
126  background: rgba(0, 255, 255, 0.05);
127}
128.config-info { margin-top: 15px; }
129.config-info pre {
130  font-size: 10px;
131  color: #888;
132  background: #1a1e37;
133  padding: 8px;
134}
135.content {
136  background: rgba(0, 180, 216, 0.05);
137  border: 1px solid #1a1e37;
138  padding: 15px;
139}
140.child-page { min-height: 200px; }
141.stats { display: flex; gap: 15px; margin-top: 15px; flex-wrap: wrap; }
142.stat {
143  background: #1a1e37;
144  padding: 10px 15px;
145  border: 1px solid #00b4d8;
146}
147.stat .label { display: block; color: #888; font-size: 11px; }
148.stat .value { display: block; color: #00ff88; font-size: 18px; margin-top: 4px; }
149.stat .value.ok { color: #00ff88; }
150ul { color: #c0c0c0; margin-top: 10px; }
151li { margin: 5px 0; }
152</style>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you define a dynamic parameter in a Vue Router path?

  2. 2. How do you access the current route's parameters in the Composition API?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create a route /system/:id and a component that reads params.id via useRoute().

  • Vertical ordering

    Arrange the steps for handling a dynamic route:

  • Click in order

    Arrange the programmatic navigation with parameters:

  • Code editor

    Navigate with query params and read them in a component via useRoute().query.

  • Horizontal ordering

    Arrange the RouterLink syntax with a dynamic parameter:

Useful articles