Fix: Vue Router Params Not Updating — Component Not Re-rendering or beforeRouteUpdate Not Firing
Quick Answer
How to fix Vue Router params not updating when navigating between same-route paths — watch $route, beforeRouteUpdate, onBeforeRouteUpdate, and component reuse behavior explained.
The Problem
Navigating from /users/1 to /users/2 doesn’t update the component:
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = route.params.id // Still shows '1' after navigating to /users/2
</script>Or beforeRouteUpdate never fires:
export default {
beforeRouteUpdate(to, from, next) {
// Never called when navigating from /posts/1 to /posts/2
this.fetchPost(to.params.id)
next()
}
}Or a lifecycle hook like onMounted doesn’t re-run when the route changes:
<script setup>
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
onMounted(async () => {
await fetchData(route.params.id) // Only called once — doesn't refetch on navigation
})
</script>Why This Happens
Vue Router reuses the same component instance when navigating between routes that match the same component. This is an optimization — creating and destroying components on every navigation is expensive — but it means:
- Lifecycle hooks don’t re-fire —
onMounted,created,beforeMountonly run when the component is first created, not when route params change. route.paramsis reactive, but you may not be watching it — readingroute.params.idonce outside a reactive context gives a snapshot, not a live value.beforeRouteUpdateis the intended solution — but it’s easy to forget to callnext(), which stalls navigation.
Fix 1: Watch route.params for Reactive Updates
The most straightforward fix: watch route.params and refetch data when it changes.
<!-- Composition API (Vue 3) -->
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
async function fetchUser(id) {
// fetch logic
}
// Watch a specific param
watch(
() => route.params.id,
async (newId, oldId) => {
if (newId !== oldId) {
await fetchUser(newId)
}
},
{ immediate: true } // Also runs on first mount
)
</script>Watch the full route object when multiple params can change:
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.params,
async (params) => {
await fetchData(params)
},
{ immediate: true, deep: true }
)
// Watch both params and query strings
watch(
() => ({ params: route.params, query: route.query }),
async ({ params, query }) => {
await fetchData(params.id, query.tab)
},
{ immediate: true, deep: true }
)
</script>Options API equivalent:
export default {
watch: {
'$route.params.id': {
immediate: true,
handler(newId) {
this.fetchUser(newId)
}
}
}
}Fix 2: Use onBeforeRouteUpdate (Composition API)
onBeforeRouteUpdate is the Composition API equivalent of the beforeRouteUpdate navigation guard:
<script setup>
import { ref } from 'vue'
import { onBeforeRouteUpdate } from 'vue-router'
const post = ref(null)
async function fetchPost(id) {
const response = await fetch(`/api/posts/${id}`)
post.value = await response.json()
}
// Called when the route changes but the component is reused
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
await fetchPost(to.params.id)
}
})
// Still need to fetch on initial mount
onMounted(() => fetchPost(route.params.id))
</script>Options API with beforeRouteUpdate:
export default {
data() {
return { post: null }
},
async mounted() {
await this.fetchPost(this.$route.params.id)
},
// Called when navigating from /posts/1 to /posts/2 (same component, new params)
async beforeRouteUpdate(to, from, next) {
await this.fetchPost(to.params.id)
next() // MUST call next() or navigation stalls
},
methods: {
async fetchPost(id) {
const response = await fetch(`/api/posts/${id}`)
this.post = await response.json()
}
}
}Warning: If you use
beforeRouteUpdatein Options API, you must callnext(). Forgetting it freezes navigation — the URL updates but no further navigation is possible.
Fix 3: Force Component Re-creation with :key
If re-using the component is causing more problems than it solves, force Vue to destroy and recreate it by binding :key to the route param:
<!-- In the parent or App.vue — bind key to the route -->
<template>
<router-view :key="$route.fullPath" />
</template>More targeted — only re-create for specific routes:
<template>
<router-view :key="routeKey" />
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const routeKey = computed(() => {
// Only force re-creation for user profile routes
if (route.name === 'UserProfile') {
return route.params.id
}
// Reuse component for other routes
return route.name
})
</script>Note: Using
:key="$route.fullPath"recreates every component on every navigation, including parent layouts. This can cause unwanted layout re-mounts. PreferwatchoronBeforeRouteUpdatefor most cases.
Fix 4: Use Computed Properties for Reactive Param Access
Instead of reading params once, use computed to stay reactive:
<!-- WRONG — snapshot, not reactive -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = route.params.id // Plain string — won't update
// template: {{ userId }} — always shows initial value
</script>
<!-- CORRECT — computed stays reactive -->
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = computed(() => route.params.id) // Ref — updates automatically
// template: {{ userId }} — updates when route changes
</script>Typed route params with Vue Router 4.4+:
// router/index.ts — typed routes
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/users/:id',
name: 'UserProfile',
component: () => import('@/views/UserProfile.vue'),
props: true // Pass params as component props
}
]
})
// UserProfile.vue — receive params as props (always up-to-date)
<script setup>
const props = defineProps<{
id: string // Received from route params via props: true
}>()
// Watch props instead of route.params
watch(() => props.id, async (newId) => {
await fetchUser(newId)
}, { immediate: true })
</script>Fix 5: Pattern for Paginated or Filtered Routes
A common scenario: a list page with query params for page/filter:
<!-- /products?page=2&category=electronics -->
<script setup>
import { ref, watch, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const products = ref([])
const loading = ref(false)
// Reactive query params
const currentPage = computed(() => Number(route.query.page) || 1)
const category = computed(() => route.query.category || '')
// Fetch when query changes
watch(
[currentPage, category],
async ([page, cat]) => {
loading.value = true
try {
products.value = await fetchProducts({ page, category: cat })
} finally {
loading.value = false
}
},
{ immediate: true }
)
// Update URL when user changes filter
function setPage(page) {
router.push({ query: { ...route.query, page } })
}
function setCategory(cat) {
router.push({ query: { ...route.query, category: cat, page: 1 } })
}
</script>Fix 6: Global Navigation Guard for Consistent Data Loading
For data that needs to be loaded before the route renders, use beforeEnter or a global beforeEach:
// router/index.ts
const router = createRouter({ /* ... */ })
// Per-route guard
const routes = [
{
path: '/users/:id',
component: UserProfile,
async beforeEnter(to, from) {
// Called on initial navigation AND when params change
// (but NOT when reusing component — use beforeRouteUpdate for that)
try {
const user = await fetchUser(to.params.id)
to.meta.user = user // Pass data via meta
} catch {
return { name: 'NotFound' } // Redirect if user not found
}
}
}
]
// UserProfile.vue — access data from meta
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const user = route.meta.user // Pre-loaded by beforeEnter
</script>Combine beforeEnter and beforeRouteUpdate for complete coverage:
// routes/user.js
const userRouteConfig = {
path: '/users/:id',
component: UserProfile,
async beforeEnter(to) {
// Runs on first navigation to this route
await loadUser(to.params.id, to)
}
}
// UserProfile.vue
onBeforeRouteUpdate(async (to) => {
// Runs when navigating between /users/1 → /users/2
await loadUser(to.params.id, to)
})Still Not Working?
watch with immediate: true but data still stale on first render — if watch with immediate: true fires asynchronously and your template renders before the data loads, show a loading state:
<template>
<div v-if="loading">Loading...</div>
<div v-else>{{ user?.name }}</div>
</template>
<script setup>
const user = ref(null)
const loading = ref(true)
watch(
() => route.params.id,
async (id) => {
loading.value = true
user.value = await fetchUser(id)
loading.value = false
},
{ immediate: true }
)
</script>Navigation guard works but component data is from the previous route — beforeRouteUpdate receives to and from as the new and old routes. Make sure you’re using to.params.id, not this.$route.params.id (which may not be updated yet when the guard fires).
router-link with same destination doesn’t navigate — Vue Router ignores navigation to the current route by default. If a user clicks a <router-link> for the current page, nothing happens. To force a reload, either add :key to <router-view> or handle it explicitly with router.push() and catch the NavigationDuplicated error.
Params disappear after programmatic navigation — when using router.push({ name: 'Route' }) without specifying params, all params reset. Always include current params if you want to preserve them: router.push({ name: 'Route', params: { ...route.params, newParam: 'value' } }).
For related Vue Router issues, see Fix: Vue Router 404 on Refresh and Fix: Vue Router Navigation Guard.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Nuxt Not Working — useFetch Returns Undefined, Server Route 404, or Hydration Mismatch
How to fix Nuxt 3 issues — useFetch vs $fetch, server routes in server/api, composable SSR rules, useAsyncData, hydration errors, and Nitro configuration problems.
Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible
How to fix Vue 3 slot issues — v-slot syntax, named slots, scoped slots passing data, default slot content, fallback content, and dynamic slot names.
Fix: Vue Teleport Not Rendering — Content Not Appearing at Target Element
How to fix Vue Teleport not working — target element not found, SSR with Teleport, disabled prop, multiple Teleports to the same target, and timing issues.
Fix: Vue Composition API Reactivity Lost — Destructured Props or Reactive Object Not Updating
How to fix Vue Composition API reactivity loss — destructuring reactive objects, toRefs, storeToRefs, ref vs reactive, watch vs watchEffect, and template not updating.