Skip to content

Fix: Vue Router Params Not Updating — Component Not Re-rendering or beforeRouteUpdate Not Firing

FixDevs ·

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-fireonMounted, created, beforeMount only run when the component is first created, not when route params change.
  • route.params is reactive, but you may not be watching it — reading route.params.id once outside a reactive context gives a snapshot, not a live value.
  • beforeRouteUpdate is the intended solution — but it’s easy to forget to call next(), 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 beforeRouteUpdate in Options API, you must call next(). 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. Prefer watch or onBeforeRouteUpdate for 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 routebeforeRouteUpdate 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles