Route Parameters

Match dynamic URL segments, read them with useRoute, and keep a reused component in step when only the parameter changes.

Syntax{ path: '/users/:id', name: 'user', component: UserView, props: true }

A route parameter is a named, variable part of a path. Write it with a colon: /users/:id matches /users/1 and /users/ada, and the matched value lands in route.params.id. One route record then serves every user instead of one record per user.

Reading a parameter

In a component, useRoute() returns a reactive object describing the current location: params, query, hash, path, fullPath, name and meta. Read a parameter inside a computed and anything derived from it updates by itself. Parameters are always strings, never numbers, and a repeatable parameter such as :chapter+ is an array of strings, so a value heading for a numeric API needs Number(...).

Reacting when only the parameter changes

Moving from /users/1 to /users/2 matches the same record, so the router reuses the component instance. It is not unmounted and not remounted: onMounted does not run again, and work done once at setup never repeats. Derive from the parameter with computed, or, for a side effect, watch(() => route.params.id, ...) with immediate: true, or the in-component guard onBeforeRouteUpdate((to, from) => ...).

Parameters as props

A component that takes its id as a prop is easier to test and reuse than one that reaches for useRoute. Set props: true on the record and every parameter is passed as a prop. That is shorthand for the function form, props: (route) => ({ id: route.params.id }), which is the one to use when you want to rename a value, pass something from the query, or coerce a type.

Example

Example · vue
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'

interface Member {
  id: string
  name: string
  role: string
}

const members: Member[] = [
  { id: '1', name: 'Ada Lovelace', role: 'admin' },
  { id: '2', name: 'Grace Hopper', role: 'editor' },
]

const route = useRoute()
const memberId = computed(() => String(route.params.id))
const member = computed<Member | undefined>(() =>
  members.find((candidate) => candidate.id === memberId.value),
)
</script>

<template>
  <section>
    <template v-if="member">
      <h1>{{ member.name }}</h1>
      <p>Role: {{ member.role }}</p>
    </template>
    <p v-else>No member with id {{ memberId }}</p>
  </section>
</template>

<style scoped>
section {
  padding: 1rem;
}
</style>

When to use it

  • Show a detail screen for any record from one route, driven by the id in the URL
  • Fix a detail page that keeps showing the first record after the user clicks a second one
  • Render a 404 screen for unmatched URLs without losing the address the visitor typed

More examples

Taking the parameter as a prop

With props: true the parameter arrives as an ordinary prop, typed as a string because that is what a parameter always is. The component can now be mounted in a test or reused inside a modal with no route at all. props: true and props: (route) => ({ id: route.params.id }) are the same thing; write the function form when you need to rename a value or pull one out of route.query.

Example · vue
<script setup lang="ts">
import { computed } from 'vue'

// route record: { path: '/orders/:id', name: 'order',
//   component: OrderView, props: true }
const props = defineProps<{ id: string }>()

const heading = computed(() => `Order ${props.id}`)
</script>

<template>
  <section>
    <h1>{{ heading }}</h1>
    <p>This component never mentions the router.</p>
  </section>
</template>

<style scoped>
section {
  padding: 1rem;
}
</style>

Running work again on a parameter change

onBeforeRouteUpdate fires when the same record is matched with different parameters, before the new location is confirmed, and it receives to and from. It is the right place when you need the old and new values together, or when the work must finish before the screen changes. A watch on route.params with immediate: true is the simpler choice when you only care about the new value and the first run.

Example · vue
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteUpdate, useRoute } from 'vue-router'

const route = useRoute()
const trail = ref<string[]>([String(route.params.slug)])

onBeforeRouteUpdate((to) => {
  trail.value.push(String(to.params.slug))
})
</script>

<template>
  <section>
    <h1>{{ trail[trail.length - 1] }}</h1>
    <ol>
      <li v-for="slug in trail" :key="slug">{{ slug }}</li>
    </ol>
  </section>
</template>

<style scoped>
section {
  padding: 1rem;
}
</style>

Parameter patterns in route records

The suffixes change how many segments a parameter eats: ? makes it optional, + takes one or more, * takes zero or more. A repeatable parameter is a string array, not a string, which is the usual source of a surprise in TypeScript. Parentheses hold a custom pattern, so :pathMatch(.*)* is a parameter named pathMatch that matches any remaining path. Writing it last is a readability convention, not a requirement: Vue Router ranks records by how specific their paths are rather than by their position in the array, so a catch-all loses to every more specific record wherever you put it. What does matter is keeping a match-everything parameter at the end of the path, because a repeatable (.*) followed by more path produces a very slow regex.

Example · ts
// src/router/content.ts
import type { RouteRecordRaw } from 'vue-router'

export const contentRoutes: RouteRecordRaw[] = [
  // one required segment
  { path: '/orders/:id', name: 'order', component: () => import('../views/OrderView.vue'), props: true },

  // optional: matches /archive and /archive/2026
  { path: '/archive/:year?', name: 'archive', component: () => import('../views/ArchiveView.vue') },

  // repeatable: /docs/guide/forms gives chapter as ['guide', 'forms']
  { path: '/docs/:chapter+', name: 'docs', component: () => import('../views/DocsView.vue') },

  // catch-all, last: anything unmatched above
  { path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('../views/NotFoundView.vue') },
]

Discussion

  • Be the first to comment on this lesson.