Setting the file. One moment.
Skill 15 · Vue Router Best Practices
Subchapter 15.6
reference/router-param-change-no-lifecycle.mdMarkdown5 KBView on GitHub
Impact: HIGH - When navigating between routes that use the same component (e.g., /users/1 to /users/2), Vue Router reuses the existing component instance for performance. This means , , and other lifecycle hooks do NOT fire, leaving you with stale data from the previous route.
onMountedcreatedwatch on route params for data fetchingonBeforeRouteUpdate in-component guard:key="route.params.id" to force re-creation (less efficient)onMounted for route-param-dependent data<!-- UserProfile.vue - Used for /users/:id -->
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const user = ref(null)
// BUG: Only runs once when component first mounts!
// Navigating from /users/1 to /users/2 does NOT trigger this
onMounted(async () => {
user.value = await fetchUser(route.params.id)
})
</script>
<template>
<div>
<!-- Still shows User 1 data when navigating to /users/2! -->
<h1>{{ user?.name }}</h1>
</div>
</template>Scenario:
/users/1 - Component mounts, fetches User 1 data/users/2 - Component is REUSED, onMounted doesn’t run<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const user = ref(null)
const loading = ref(false)
// Watch for param changes - handles both initial load and navigation
watch(
() => route.params.id,
async (newId) => {
loading.value = true
user.value = await fetchUser(newId)
loading.value = false
},
{ immediate: true } // Run immediately for initial load
)
</script><script setup>
import { ref, onMounted } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
const route = useRoute()
const user = ref(null)
async function loadUser(id) {
user.value = await fetchUser(id)
}
// Initial load
onMounted(() => loadUser(route.params.id))
// Handle param changes within same route
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
await loadUser(to.params.id)
}
})
</script><!-- App.vue or parent component -->
<template>
<router-view :key="$route.fullPath" />
</template>Tradeoffs:
// composables/useRouteData.js
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
export function useRouteData(paramName, fetcher) {
const route = useRoute()
const data = ref(null)
const loading = ref(false)
const error = ref(null)
watch(
() => route.params[paramName],
async (id) => {
if (!id) return
loading.value = true
error.value = null
try {
data.value = await fetcher(id)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
},
{ immediate: true }
)
return { data, loading, error }
}<!-- Usage in component -->
<script setup>
import { useRouteData } from '@/composables/useRouteData'
import { fetchUser } from '@/api/users'
const { data: user, loading, error } = useRouteData('id', fetchUser)
</script>| Navigation Type | Lifecycle Hooks | beforeRouteUpdate | Watch on params |
|---|---|---|---|
/users/1 to /posts/1 | YES | NO | YES |
/users/1 to /users/2 | NO | YES | YES |
/users/1?tab=a to /users/1?tab=b | NO | YES | NO (different watch) |
/users/1 to /users/1 (same) | NO | NO | NO |
watch with immediate: true - Covers both initial load and updatesonBeforeRouteUpdate is navigation-aware - Good for data that must load before view updates:key="route.fullPath" is a sledgehammer - Use only when necessary