skip to content

Upgrading a Vue 2 admin app from Vue Router 3 to Vue Router 5, which router setup code breaks, and what does Vue Router 5 itself change?

level: seniorimportance: should knowfreq 38%

answer

  1. functions, not a class
  2. mode and base move into the history
  3. onReady becomes a promise
  4. currentRoute is a ref
  5. v5: transition release, next() warns

basics

~20 s

Vue Router 4 broke the setup: new VueRouter becomes createRouter, mode and base become a history factory and its argument, onReady becomes isReady, and currentRoute becomes a ref; Vue Router 5 adds no breaks for a plain v4 app.

solid answer

~40 s

The breaks all come from the v4 rewrite for Vue 3. `new VueRouter({...})` with `Vue.use` becomes `createRouter({...})` with `app.use(router)`, and `routes` is required. `mode: 'history' | 'hash' | 'abstract'` becomes `history: createWebHistory() | createWebHashHistory() | createMemoryHistory()`, `base` becomes the factory's argument and `fallback` is gone. `router.onReady(ok, err)` becomes the promise `router.isReady()`, and since the first navigation is now always async you may await it before mount. `router.currentRoute` is a ref, `router.app`, `router.match` and `getMatchedComponents()` are gone, and pushing an unknown named route now throws. Vue Router 5 is a transition release: a v4 app without the file-routing plugin upgrades with no code change, while `next()` in guards now warns in dev and Vue Router 6 will be ESM-only and drop deprecated APIs.

code

ts · 18 lines
ts
// Vue Router 3 (Vue 2), before:
// Vue.use(VueRouter)
// const router = new VueRouter({ mode: 'history', base: '/admin/', routes })
// router.onReady(() => new Vue({ router, render: h => h(App) }).$mount('#app'))

// Vue Router 5 (Vue 3), after:
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import { routes } from './routes'

const router = createRouter({
  history: createWebHistory('/admin/'),
  routes,
})

const app = createApp(App).use(router)
router.isReady().then(() => app.mount('#app'))

go deeper

for a junior

Know that Vue Router 4 and later use createRouter with a history factory, and that isReady() replaced onReady().

for a middle

List the setup breaks from v3 to v4 and explain the async first navigation and currentRoute being a ref.

for a senior

Plan the router part of a Vue 2 upgrade: separate compile-time breaks from quiet runtime ones, and use Vue Router 5's warnings to prepare for 6.

for a principal

Sequence the router upgrade against the rest of the Vue 3 migration and decide how much deprecated usage the codebase may carry into the next major.

## Why the setup broke Vue Router 4 was rewritten for Vue 3's app model, where plugins are installed per app with `app.use()` and there is no global `Vue` constructor. The router stopped being a class and became a **set of functions**. Most route-table syntax survived; the **setup code** around it did not. Vue Router 5 then added almost nothing breaking on top of v4, so a Vue 2 app going to v5 today faces the v3 to v4 list plus a short v5 note. ## Construction and history | Vue Router 3 | Vue Router 4 and 5 | |---|---| | `Vue.use(VueRouter)` + `new VueRouter({...})` | `createRouter({...})` + `app.use(router)` | | `mode: 'history'` | `history: createWebHistory()` | | `mode: 'hash'` | `history: createWebHashHistory()` | | `mode: 'abstract'` | `history: createMemoryHistory()` | | `base: '/admin/'` | `createWebHistory('/admin/')` | | `fallback: false` | removed | | `routes` optional | `routes` required | | `router.onReady(ok, err)` | `router.isReady()` promise | The reasons given for the history change are tree shaking of unused histories and support for custom histories. `fallback` went because every browser Vue 3 supports has the History API. ## Runtime behaviour that changes quietly These compile but behave differently, which makes them the costly part of an upgrade: 1. **Every navigation is asynchronous**, including the first. Code that read the route right after startup, or a route transition, may need `await router.isReady()` before `mount()`. 2. **`router.currentRoute` is a `Ref`**: `router.currentRoute.query` is now `undefined`; read `router.currentRoute.value.query`. `$route` and `useRoute()` are unaffected. 3. **Unknown named routes throw**: `router.push({ name: 'userz' })` used to go to `/` and render nothing; now it throws, as does a named route missing a required param. 4. **`router.app` is gone**, because one router can serve several apps. 5. **`router.match` merged into `router.resolve`**, and `router.getMatchedComponents()` is gone; read `router.currentRoute.value.matched` instead. 6. **The router owns `history.state`**: code that calls `history.pushState()` or `history.replaceState()` itself must keep the router's state, for example `history.replaceState(history.state, '', url)`. 7. **TypeScript names changed**: `RouteConfig` is `RouteRecordRaw`, `Route` is `RouteLocationNormalized`, `Location` is `RouteLocation`. ## Breaks that belong to other parts of the router The same upgrade touches areas taught elsewhere; list them in the plan but fix them with their own rules: - the `*` catch-all becomes a param with a custom regex, and unnamed params are gone; - `scrollBehavior` returns `left` and `top` instead of `x` and `y`; - `<transition>` and `<keep-alive>` move inside `RouterView`'s `v-slot`; - `RouterLink` loses `tag`, `event`, `exact` and `append`; - `push()` loses its `onComplete` and `onAbort` callbacks in favour of the returned promise; - guards declared in mixins are ignored. ## What Vue Router 5 changes - **No breaking changes for a v4 app without file-based routing**: update the dependency and it runs. The one exception is the IIFE build, which no longer bundles the devtools API. - **File-based routing moved into the core package**: projects on the old separate plugin change import paths to `vue-router/vite` and `vue-router/auto-routes`. - **The `next` guard argument is deprecated**, typed `@deprecated` and, since 5.0.3, warned about in development; guards should return a value instead. - The package became `"type": "module"` in 5.0.3 while still shipping CommonJS entry points. - The team's stated plan is that **Vue Router 6 will be ESM-only and remove deprecated APIs**, so v5 is the runway to clear `next()` and other deprecated calls. - A late 4.x change worth checking on upgrade: since 4.5 the router **throws when a parent and a child route share a name**. - Vue Router 5 declares Vue `^3.5.34` as its peer, so Vue itself must be on a recent 3.5 release. ## Order of work inside the router 1. Replace construction and history (`createRouter`, factory, base). 2. Replace `onReady` with `isReady()` and decide whether to await it before mount. 3. Fix `currentRoute.value`, removed instance members and types. 4. Run the app and fix the throws from unknown or under-specified named routes. 5. Clear deprecated `next()` calls so the dev console is quiet before Vue Router 6. ## Proving it worked The compile-time breaks announce themselves; the quiet ones need evidence. Before the upgrade merges: - run unit tests that push every named route with and without its params, so missing-param throws surface in CI rather than in production; - load each top-level route by URL, not only by clicking, because the first navigation now runs asynchronously and through the guards; - watch the development console, where Vue Router 5 reports deprecated usage and route-table problems with coded warnings; - search the codebase for `currentRoute.` without `.value`, for `onReady`, and for direct `history.pushState` calls.

  • After the upgrade, a sidebar computed reads `this.$router.currentRoute.path` and is always undefined. Why, and what is the fix?
    In Vue Router 4 and later `currentRoute` is a `Ref`, so the location sits at `currentRoute.value`. Read `this.$router.currentRoute.value.path`, or better `this.$route.path` or `useRoute().path`, which already unwrap it and stay reactive.
  • The team is on Vue Router 4 today. What would you do now to make Vue Router 6 cheap?
    Upgrade to 5, which needs no code change for a plain v4 app, then clear what 5 flags: rewrite guards that call `next()` to return values, stop relying on CommonJS `require` of the router, and move any separate file-routing plugin imports to `vue-router/vite`. The dev warnings are the checklist.
  • The Vue 2 app pushes `{ name: 'user-detail' }` from a table without passing `id`. What changes on Vue Router 5?
    Vue Router 3 resolved it loosely; since Vue Router 4, pushing or resolving a named route without its required params throws. Pass `params: { id }` explicitly, or push a path string. The upgrade surfaces these as runtime errors, so exercise every navigation path in tests.

saying these in an interview costs you the question

  • new VueRouter() still works in Vue Router 5 through a compatibility export.
  • base stays a router option; only mode changed.
  • Vue Router 5 removed next(), so every guard must be rewritten before upgrading.
  • router.currentRoute.path works as before because the route object is unchanged.
  • Upgrading from 4 to 5 requires moving routes to file-based routing.