Watch * routes, optional params, and navigation duplication (guard calling next incorrectly) — these are the “works in dev, loops in edge cases in prod” class of bugs.
1. createWebHistory vs hash mode
If you move between modes for SEO or static hosting, redirects and 404 fallbacks on the server must be updated the same day.
2. scrollBehavior typing & async
Validate deep links and back-button scroll restoration on long lists—especially in strangler setups with mixed shells.
3. router-view and named views
Layout composition changed slightly in ergonomics; nested routes with many named outlets need a diagram before refactor.
4. The breaking changes you will actually hit
The Vue Router 4 changelog is long, but the changes that consume schedule cluster into a small set. Here is the running list we keep in front of every migration.
Router instantiation
new VueRouter() becomes createRouter(); mode: 'history' becomes history: createWebHistory(). Trivial — but the search-and-replace catches most of the mechanical work.
Wildcard routes
The catch-all changes from path: '*' to path: '/:pathMatch(.*)*'. If your 404 page or marketing redirects use a *, this is a one-line fix that silently breaks 404 pages until you find it.
Optional and repeated params
The /foo/:bar? syntax now needs /foo/:bar(.*)? in many cases, and */+ repeat modifiers behave subtly differently. Test deep links with empty and multi-segment params explicitly.
Navigation guards
Returning a value (or a Promise) from a guard is now the recommended pattern; the next() callback still works but is easy to call twice during refactors. Calling next with both an argument and not is the #1 source of “navigation aborted” loops we debug.
router-link active class behavior
Active matching is now strictly path-segment based. Designs that relied on partial matches need exact-active-class or a v-slot custom render — common in nav components built years ago.
In-component $route access
In <script setup> you use useRoute() and useRouter(); this.$route still works in Options API, but composables become the idiom and your team will inevitably mix both for a while.
5. Before/after: the router you almost certainly have
Most Vue 2 routers we see look something like:
// Vue Router 3
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/users/:id?', component: UserPage, meta: { auth: true } },
{ path: '*', component: NotFound },
],
})
router.beforeEach((to, from, next) => {
if (to.meta.auth && !store.state.user) next('/login')
else next()
})The Vue Router 4 equivalent:
// Vue Router 4
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/users/:id?', component: UserPage, meta: { auth: true } },
{ path: '/:pathMatch(.*)*', component: NotFound },
],
})
router.beforeEach((to) => {
if (to.meta.auth && !useUserStore().isAuthed) return '/login'
})Mechanically small. The work that consumes weeks lives around these changes — guard rewrites against a new Pinia store, scroll behavior on long lists, layout regressions in named views, and SSR redirects.
6. Where the schedule actually slips
- Auth flows — every guard touches user state, which is also moving from Vuex to Pinia. Two refactors collide on the same files.
- Analytics — page-view tracking lives in
afterEach; subtle URL normalization changes can double-count or miss events. - SSR / static export —
historymode and 404 fallbacks need server-side updates the same release. See SSR & hydration. - Deep links from emails — old query parameter shapes deserve a redirect map; do not rely on “users will figure it out.”
- Named views and layouts — many Vue 2 apps use
router-view name="sidebar"patterns that are easier to refactor into layout components in Vue 3 than to port verbatim. - Transitions —
<transition>wrapping<router-view>needs the v-slot pattern in Vue 3 to avoid double-mount.
Plan a dedicated router sprint, not “a few days after components.” Realistic timelines for a mid-size SPA: 2–4 sprints of router-related work spread across the migration. We dig into ranges in migration timelines by project size.
7. A staged rollout that keeps prod stable
- Inventory routes. Export the full route table to a doc; every product manager reviewing a route change should be able to find their feature in it.
- Mechanical migration.
createRouter, history mode, wildcard syntax — all in one PR with no behavior changes intended. - Guard refactor. Convert
next()calls to return-style guards. One module per PR. - Composable migration. Replace
this.$routeusage withuseRoute()incrementally, starting with new code. - Server redirects. Add 301s for any deep-link shape changes; SEO does not forgive a quiet rename.
- Smoke pack. Cypress or Playwright suite that hits 30+ canonical routes plus 5 known-bad inputs (trailing slashes, double encoding, hash-only URLs).
8. Anti-patterns we keep seeing
- One mega guard. A
beforeEachwith 200 lines of conditionals. Split into named guards or per-routebeforeEnterhooks; bugs become testable. - Sync state writes from guards. Guards that mutate Pinia/Vuex state cause subtle reactivity bugs after navigation cancellation. Read state, return a redirect — don’t mutate during the guard.
- Skipping
router.isReady()in tests. Tests pass locally, fail under CI parallelism. Always await readiness in component tests. - Hash mode “for SEO.” Hash mode was always invisible to bots. If SEO matters, plan history mode plus server-side fallbacks.
- Hardcoded paths in templates. A migration is a great moment to centralize route names and use
name + paramsinstead of string paths.
9. FAQ
Can we run Vue Router 3 with Vue 3?
Not in production. Vue Router 4 is the only supported version on Vue 3. There is no compat shim that we trust.
How do we migrate the router before the rest of the app?
You generally don’t. The router migration is part of moving the app to Vue 3. What you can do early is harden the route table, name everything, and clean up dead routes — that work pays off whichever order you choose. The order conversation is part of the broader DIY Vue 2 to 3 roadmap.
What about Nuxt Bridge and Nuxt 3?
Nuxt’s file-based router abstracts most of these concerns, but the underlying breaking changes still apply for any custom guards or middleware.
How do we keep stakeholders calm?
Show the route inventory, the guard refactor plan, and the smoke pack. Router work feels invisible to product owners; making the artifacts visible buys you the schedule space — see scope freeze without losing the business.
Conclusion
Vue Router 4 is the spine of the SPA. Under-estimate it and your “Vue 3 done” date slips a sprint at a time with tiny, painful navigation bugs. Plan it, test it, and ship with confidence.
