Skip to the solution
MediumFrontend10 min read

How do dynamic routes and route params work in Vue Router?

1.0k views

The problem

Question presented to candidate: "We have a user profile page at /users/:id. Explain how dynamic segments and params work in Vue Router, how the page gets the id, and what goes wrong when the user clicks from one profile straight to another."

What a strong answer should cover:

  • A :name segment in a route path matches one URL segment and exposes it as route.params.name, always a string (or an array for repeatable params).
  • Reading params: useRoute().params, or better, props: true on the route so the component takes the param as a prop and stays router-agnostic.
  • Advanced patterns: custom regex (:id(\d+)), optional (?), repeatable (+ / *), and the catch-all /:pathMatch(.*)*.
  • Component reuse: going from /users/1 to /users/2 keeps the same instance, so onMounted does not run again; react with watch on the param or onBeforeRouteUpdate, or force a remount with :key.
  • Building links with named routes and params (encoded for you) instead of string concatenation; a missing required param throws.

Clarifying questions expected:

  • "Should a profile-to-profile switch keep scroll position and local state, or start fresh?"
  • "Are ids numeric only, so we could reject /users/abc at the router level?"

Code / implementation expected: Yes β€” a route with :id and props: true, and a component that reloads data when the param changes.

  • router
  • params

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

VueWhy a param change does not re-run onMounted (no router needed)
<script setup>
// Why /users/1 -> /users/2 does not re-run onMounted: Vue reuses a component
// when only its props change. RouterView does the same when only a param
// changes. Here the "param" is a plain ref so the demo needs no router.
import { ref, watch, onMounted, defineComponent, h } from "vue";

const log = ref([]);

const UserPage = defineComponent({
  props: { id: Number, label: String },
  setup(props) {
    const loadedFor = ref(null);
    onMounted(() => {
      log.value.push(`${props.label}: mounted, load user ${props.id}`);
      loadedFor.value = props.id;
    });
    return () => h("p", `${props.label} shows id ${props.id}, data loaded for ${loadedFor.value}`);
  },
});

const WatchingUserPage = defineComponent({
  props: { id: Number, label: String },
  setup(props) {
    const loadedFor = ref(null);
    watch(
      () => props.id,
      (id) => {
        log.value.push(`${props.label}: watch fired, load user ${id}`);
        loadedFor.value = id;
      },
      { immediate: true },
    );
    return () => h("p", `${props.label} shows id ${props.id}, data loaded for ${loadedFor.value}`);
  },
});

const userId = ref(1);
</script>

<template>
  <button @click="userId++">Go to /users/{{ userId + 1 }}</button>
  <UserPage :id="userId" label="A (onMounted only)" />
  <WatchingUserPage :id="userId" label="B (watch the param)" />
  <UserPage :key="userId" :id="userId" label="C (:key forces remount)" />
  <ol>
    <li v-for="(line, i) in log" :key="i">{{ line }}</li>
  </ol>
</template>
VueUserPage.vue: param as a prop, watched so /users/1 -> /users/2 reloads
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { onBeforeRouteUpdate } from "vue-router";
import { fetchUser, type User } from "./api";

// Arrives as a prop because the route has props: true (always a string)
const props = defineProps<{ id: string }>();
const user = ref<User | null>(null);

onMounted(() => console.log(`mounted with id ${props.id}`)); // runs ONCE

// /users/1 -> /users/2 reuses this instance, so react to the param instead
watch(
  () => props.id,
  async (id) => {
    user.value = null;
    user.value = await fetchUser(id);
    console.log(`loaded user ${id}: ${user.value.name}`);
  },
  { immediate: true },
);

onBeforeRouteUpdate((to, from) => {
  console.log(`beforeRouteUpdate ${from.fullPath} -> ${to.fullPath}`);
});
</script>

<template>
  <p v-if="!user">Loading user {{ id }}...</p>
  <h1 v-else>{{ user.name }}</h1>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 29 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What is a Vue plugin?

Continue