skip to content

Param Matching & Props

How a path like /users/:id+ is matched and ranked, why a params-only change reuses the component, and passing params as props. Interviewers test catch-all 404s and props modes.

on this pageshow

explore

questions

6

In Vue Router 5, how does a route record with path '/products/:id' match URLs, and how does a script setup component read the id?

level: juniorimportance: must knowfreq 66%

answer

  1. colon marks a param
  2. one segment, no slash
  3. case and trailing slash forgiven
  4. useRoute().params, decoded strings
  5. query lives elsewhere

basics

~10 s

The :id segment matches exactly one non-empty path segment, so /products/42 matches and /products/42/reviews does not; a component reads it as useRoute().params.id, a decoded string.

solid answer

~40 s

A colon turns a segment into a **param**: `/products/:id` matches `/products/42` and `/products/blue-kettle`, but not `/products` (the param is required) or `/products/42/reviews` (a plain param never spans a `/`). By default matching is case-insensitive and tolerates a trailing slash, so `/Products/42/` also matches. In `<script setup>`, `const route = useRoute()` gives a reactive route object; `route.params.id` is the value, always a decoded **string**, typed `string | string[]` on an untyped router because repeatable params produce arrays. In templates `$route.params.id` works too. Query and hash are separate: `/products/42?tab=specs` puts `tab` in `route.query`, not in `params`. Several params can share one path, like `/shops/:shopId/products/:id`.

code

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

const route = useRoute()
// '42' for /products/42 (a string, decoded)
const productId = computed(() => String(route.params.id))
const tab = computed(() => route.query.tab ?? 'overview')
</script>

<template>
  <h1>Product {{ productId }}</h1>
  <p>Tab: {{ tab }}</p>
</template>

go deeper

for a junior

Know the colon syntax, that a plain param matches one segment, and that useRoute().params.id gives a string.

for a middle

Explain decoding, the string | string[] typing, the case and trailing-slash defaults, and why named pushes need required params.

for a senior

Spot typos like /products:id, keep conversion out of components with props functions, and prefer typed routes in a large table.

for a principal

Set conventions for param naming and typing across a large route table so URL changes cannot silently break links.

## Declaring a dynamic segment In Vue Router 5 a route record's `path` is a small pattern language. A segment that starts with a colon is a **param**: ```ts const routes = [ { path: '/products/:id', name: 'product', component: ProductPage }, ] ``` One record now serves every product page. The name after the colon, `id`, becomes the key in `route.params`. A path can hold several params, for example `/shops/:shopId/products/:id`, and each one becomes its own key. ## What the default param matches - **Exactly one segment.** The default param pattern matches one or more characters that are not `/`. `/products/42` matches; `/products/42/reviews` does not, because that would need the param to swallow a slash. - **Required.** `/products` alone does not match; the segment must be present and non-empty. Optional params use a `?` modifier, which is a separate topic. - **Case-insensitive.** The router's `sensitive` option defaults to `false`, so `/PRODUCTS/42` matches too. - **Trailing slash tolerated.** The `strict` option defaults to `false`, so `/products/42/` matches the same record. When the default is too loose, for example when `/products/new` and `/products/:id` both exist, the router ranks candidates instead of trying them in array order. That ranking, custom regular expressions in params and the `strict` and `sensitive` switches are the deeper half of this subject. ## Reading the param in a component With the Composition API, `useRoute()` returns the current route location. It is reactive, so reading from it inside a `computed`, a `watch` source or the template tracks changes. | Where | How to read it | |---|---| | `<script setup>` | `const route = useRoute()` then `route.params.id` | | Template | `$route.params.id` or `route.params.id` | | Options API | `this.$route.params.id` | | Route record | `props: true` passes it as a prop instead | A few properties of the value matter in interviews: 1. It is a **string**. `route.params.id === 42` is always false; convert with `Number()` where a number is needed. 2. It is **decoded**. A URL segment `blue%20kettle` arrives as `blue kettle`, and a `/` inside a param travels in the URL as `%2F` but reads back as `/`. 3. On a router without typed routes, `route.params` is typed as a record of `string | string[]`, because a **repeatable** param yields an array. Code that assumes a string needs a narrowing check or a typed router. 4. It holds **only path params**. `?tab=specs` lands in `route.query`, `#reviews` in `route.hash`. ## Building URLs from params The same record works in reverse. `router.push({ name: 'product', params: { id: '42' } })` or `<RouterLink :to="{ name: 'product', params: { id: product.id } }">` produces `/products/42`, encoding the value for you. A named push without `id` reuses the current route's `id` when there is one; from a page without an `id` it throws, because the router cannot build a URL with a required param missing. Hand-built strings such as `` `/products/${id}` `` work, but you must encode the value yourself and they break silently when the path is renamed. ## Common slips - Declaring `/products:id` without the slash: that is a static prefix `products` followed by a param in the **same segment**, so it matches `/products42`, not `/products/42`. - Reading `route.params.id` once into a plain variable at setup time and expecting it to update; the value is reactive only while it is read through `route`. - Expecting `route.params` to include query values. - Comparing the id with `===` against a number. ## Edge cases the matcher reports - **Duplicate names.** `/shops/:id/products/:id` triggers a development warning, and only the last `id` survives in `route.params`; give each param its own name. - **Malformed encoding.** A segment with a broken percent-escape cannot be decoded; the router warns in development and keeps the raw text instead of throwing. - **Mixed segments.** Static text and a param can share a segment on purpose: `/file-:name` matches `/file-report` with `name` equal to `report`. - **No match.** A URL that matches no record leaves `route.matched` empty and renders nothing, with a development warning, unless the table has a catch-all route for a 404 page. ## A catalogue example For a catalogue with `/products/:id` and `/shops/:shopId/products/:id`, both records can use the same `ProductPage` component. It reads `route.params.id` in both cases, and `route.params.shopId` only exists on the second. Code that needs to know which record matched should check `route.name` rather than guess from the presence of a param.

  • Why is route.params.id typed as string | string[] when the pattern has a single :id?
    On an untyped router, `route.params` has one generic type for every route, and some routes use repeatable params such as `:path+`, which produce arrays. TypeScript cannot know which record matched. Narrow the value, convert it in a `props` function, or use typed routes so each route name maps to its own param types.
  • What does /products:id match, compared with /products/:id?
    Without the slash, `products` and `:id` sit in one segment: a static prefix followed by a param. It matches `/products42` and puts `42` in `id`, but not `/products/42`. Mixed segments like `/file-:name` are allowed on purpose; here it is almost always a typo.

saying these in an interview costs you the question

  • route.params.id is a number when the URL segment is numeric.
  • A plain :id param can match several segments like 42/reviews.
  • Query string values show up in route.params.
  • Route matching is case-sensitive by default.
  • /products/42/ fails to match /products/:id because of the trailing slash.
open as a page

In Vue Router 5, clicking from /products/1 to /products/2 keeps showing product 1; why, and how do you fix the component?

level: middleimportance: must knowfreq 72%

basics

~10 s

Both URLs match the same record, so RouterView patches the existing component instead of remounting it; setup and onMounted do not run again. Watch route.params.id, or the prop from props: true, with immediate: true.

open as a page

In Vue Router 5, what do a route record's props: true, object and function modes pass to the component, and when does each fit?

level: middleimportance: should knowfreq 48%

basics

~10 s

props: true passes route.params as props, an object passes fixed props as-is, and a function receives the route and returns props, which is how you cast ids to numbers or pass query values.

open as a page

In Vue Router 5, how do a docs route '/docs/:path+' and a '/:pathMatch(.*)*' 404 route match multi-segment URLs, and what params do they produce?

level: middleimportance: should knowfreq 55%

basics

~10 s

A + param repeats over one or more segments and yields an array, so /docs/guide/setup gives ['guide', 'setup']; /:pathMatch(.) matches any path, ranks below specific routes, and yields the segments as an array.

open as a page

In Vue Router 5, a catalogue has /products/new, /products/:id(\d+) and /products/:slug; how does the router pick a route, and when does array order matter?

level: seniorimportance: should knowfreq 34%

basics

~10 s

Vue Router ranks records by a per-segment score: static segments beat params, a custom regex beats a plain param, and optional, repeatable and wildcard params score lower. Array order only breaks exact ties.

open as a page

In Vue Router 5, legacy catalogue URLs like /catalog/item/:id must keep working; when do you use a redirect record versus an alias, and what does each cost?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A redirect replaces the legacy URL with the new one before any guard runs; an alias keeps the legacy URL in the address bar while rendering the target record. Redirect retires old URLs; alias serves two permanent URLs.

open as a page