Dynamic Route Matching with Params
Learn how to create flexible routes that can match multiple URL patterns using dynamic segments.
Introduction
Very often we need to map routes with the given pattern to the same component. For example, we may have a User component which should be rendered for all users but with different user IDs. In Vue Router we can use a dynamic segment in the path to achieve that, we call that a param.
Basic Dynamic Routes
Dynamic segments are denoted by a colon : followed by the parameter name:
import User from './User.vue'
const routes = [
// Dynamic segments start with a colon
{ path: '/users/:id', component: User },
]Now URLs like /users/johnny and /users/jolyne will both map to the same route.
When a route is matched, the value of its params will be exposed as $route.params in every component:
<template>
<div>
<!-- The current route is accessible as $route in the template -->
User {{ $route.params.id }}
</div>
</template>Multiple Parameters
You can have multiple params in the same route, and they will map to corresponding fields on $route.params:
| Pattern | Matched Path | $route.params |
|---|---|---|
/users/:username | /users/eduardo | { username: 'eduardo' } |
/users/:username/posts/:postId | /users/eduardo/posts/123 | { username: 'eduardo', postId: '123' } |
Accessing Route Information
In addition to $route.params, the route object also exposes other useful information:
$route.query- URL query parameters$route.hash- URL hash$route.fullPath- Complete URL including query and hash$route.matched- Array of matched route records
Composition API
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
// Access parameters
console.log(route.params.id)
console.log(route.query)
console.log(route.hash)
</script>Options API
<script>
export default {
created() {
console.log(this.$route.params.id)
console.log(this.$route.query)
console.log(this.$route.hash)
}
}
</script>Reacting to Parameter Changes
When the user navigates from /users/johnny to /users/jolyne, the same component instance will be reused. Since both routes render the same component, this is more efficient than destroying the old instance and creating a new one. However, this also means that component lifecycle hooks will not be called.
Using Watchers
To react to params changes in the same component, you can watch the route object:
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.params.id,
async (newId, oldId) => {
// React to route changes
console.log(`User changed from ${oldId} to ${newId}`)
// Fetch new user data
await fetchUserData(newId)
}
)
</script>Using Navigation Guards
Alternatively, use the beforeRouteUpdate navigation guard:
<script setup>
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate(async (to, from) => {
// React to route changes
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
</script>Catch-all / 404 Routes
Regular params will only match characters between URL fragments, separated by /. If we want to match anything, we can use a custom param regexp:
const routes = [
// Will match everything and put it under `$route.params.pathMatch`
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// Will match anything starting with `/user-`
{ path: '/user-:afterUser(.*)', component: UserGeneric },
]404 Not Found Example
<!-- NotFound.vue -->
<template>
<div>
<h1>404 - Page Not Found</h1>
<p>The page "{{ $route.params.pathMatch }}" could not be found.</p>
<router-link to="/">Go Home</router-link>
</div>
</template>Programmatic Navigation to Catch-all Routes
When navigating to a catch-all route programmatically, you need to provide the path as an array:
router.push({
name: 'NotFound',
// Preserve current path and remove the first char to avoid starting with `//`
params: { pathMatch: route.path.substring(1).split('/') },
// Preserve existing query and hash
query: route.query,
hash: route.hash,
})Advanced Patterns
Vue Router supports many advanced matching patterns:
- Optional parameters:
/users/:id? - Zero or more:
/files/* - One or more:
/files/+ - Custom regex:
/users/:id(\\d+)
For more details, see the Advanced Matching Patterns documentation.
Best Practices
1. Validate Parameters
Always validate route parameters before using them:
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = computed(() => {
const id = route.params.id
return /^\d+$/.test(id) ? parseInt(id) : null
})
const isValidUser = computed(() => userId.value !== null)
</script>
<template>
<div v-if="isValidUser">
User ID: {{ userId }}
</div>
<div v-else>
Invalid user ID
</div>
</template>2. Handle Loading States
Show loading indicators when fetching data based on route parameters:
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const user = ref(null)
const loading = ref(false)
const fetchUser = async (id) => {
loading.value = true
try {
user.value = await api.getUser(id)
} catch (error) {
console.error('Failed to fetch user:', error)
} finally {
loading.value = false
}
}
// Watch for parameter changes
watch(
() => route.params.id,
(newId) => {
if (newId) {
fetchUser(newId)
}
},
{ immediate: true }
)
</script>3. Use TypeScript for Better Type Safety
interface RouteParams {
id: string
}
const route = useRoute<RouteParams>()
// route.params.id is now typed as stringCommon Pitfalls
- Forgetting to watch parameter changes - Component instances are reused
- Not validating parameters - Always check if parameters are in expected format
- Incorrect catch-all patterns - Use
(.*)*for proper catch-all behavior - Missing error handling - Always handle cases where data fetching fails
Next Steps
- Learn about Nested Routes for complex layouts
- Explore Route Matching Syntax for advanced patterns
- Understand Navigation Guards for route protection