Migrating from Vue 2
Most of Vue Router API has remained unchanged during its rewrite from v3 (for Vue 2) to v4 (for Vue 3) but there are still a few breaking changes that you might encounter while migrating your application. This guide is here to help you understand why these changes happened and how to adapt your application to make it work with Vue Router 4.
Breaking Changes
Changes are ordered by their usage. It is therefore recommended to follow this list in order.
new Router becomes createRouter
Vue Router is no longer a class but a set of functions. Instead of writing new Router(), you now have to call createRouter:
// previously was
// import Router from 'vue-router'
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})New history option to replace mode
The mode: 'history' option has been replaced with a more flexible one named history. Depending on which mode you were using, you will have to replace it with the appropriate function:
"history":createWebHistory()"hash":createWebHashHistory()"abstract":createMemoryHistory()
Here is a full snippet:
import { createRouter, createWebHistory } from 'vue-router'
// there is also createWebHashHistory and createMemoryHistory
createRouter({
history: createWebHistory(),
routes: [],
})On SSR, you need to manually pass the appropriate history:
// router.js
let history = isServer ? createMemoryHistory() : createWebHistory()
let router = createRouter({ routes, history })
// somewhere in your server-entry.js
router.push(req.url) // request url
router.isReady().then(() => {
// resolve the request
})Reason: enable tree shaking of non used histories as well as implementing custom histories for advanced use cases like native solutions.
Moved the base option
The base option is now passed as the first argument to createWebHistory (and other histories):
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
history: createWebHistory('/base-directory/'),
routes: [],
})Removal of the fallback option
The fallback option is no longer supported when creating the router:
-new VueRouter({
+createRouter({
- fallback: false,
// other options...
})Reason: All browsers supported by Vue support the HTML5 History API, allowing us to avoid hacks around modifying location.hash and directly use history.pushState().
Removed * (star or catch all) routes
Catch all routes (*, /*) must now be defined using a parameter with a custom regex:
const routes = [
// pathMatch is the name of the param, e.g., going to /not/found yields
// { params: { pathMatch: ['not', 'found'] }}
// this is thanks to the last *, meaning repeated params and it is necessary if you
// plan on directly navigating to the not-found route using its name
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
// if you omit the last `*`, the `/` character in params will be encoded when resolving or pushing
{ path: '/:pathMatch(.*)', name: 'bad-not-found', component: NotFound },
]
// bad example if using named routes:
router.resolve({
name: 'bad-not-found',
params: { pathMatch: 'not/found' },
}).href // '/not%2Ffound'
// good example:
router.resolve({
name: 'not-found',
params: { pathMatch: ['not', 'found'] },
}).href // '/not/found'TIP
You don't need to add the * for repeated params if you don't plan to directly push to the not found route using its name. If you call router.push('/not/found/url'), it will provide the right pathMatch param.
Reason: Vue Router doesn't use path-to-regexp anymore, instead it implements its own parsing system that allows route ranking and enables dynamic routing. Since we usually add one single catch-all route per project, there is no big benefit in supporting a special syntax for *. The encoding of params is encoding across routes, without exception to make things easier to predict.
The currentRoute property is now a ref()
Previously the properties of the currentRoute object on a router instance could be accessed directly.
With the introduction of vue-router v4, the underlying type of the currentRoute object on the router instance has changed to Ref<RouteLocationNormalizedLoaded>, which comes from the newer reactivity fundamentals introduced in Vue 3.
While this doesn't change anything if you're reading the route with useRoute() or this.$route, if you're accessing it directly on the router instance, you will need to access the actual route object via currentRoute.value:
const { page } = router.currentRoute.query
const { page } = router.currentRoute.value.queryReplaced onReady with isReady
The existing router.onReady() function has been replaced with router.isReady() which doesn't take any argument and returns a Promise:
// replace
router.onReady(onSuccess, onError)
// with
router.isReady().then(onSuccess).catch(onError)
// or use await:
try {
await router.isReady()
// onSuccess
} catch (err) {
// onError
}scrollBehavior changes
The object returned in scrollBehavior is now similar to ScrollToOptions: x is renamed to left and y is renamed to top. See RFC.
Reason: making the object similar to ScrollToOptions to make it feel more familiar with native JS APIs and potentially enable future new options.
<router-view>, <keep-alive>, and <transition>
transition and keep-alive must now be used inside of RouterView via the v-slot API:
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>Reason: This was a necessary change. See the related RFC.
Removal of append prop in <router-link>
The append prop has been removed from <router-link>. You can manually concatenate the value to an existing path instead:
replace
<router-link to="child-route" append>to relative child</router-link>
with
<router-link :to="append($route.path, 'child-route')">
to relative child
</router-link>You must define a global append function on your App instance:
app.config.globalProperties.append = (path, pathToAppend) =>
path + (path.endsWith('/') ? '' : '/') + pathToAppendReason: append wasn't used very often, is easy to replicate in user land.
Removal of event and tag props in <router-link>
Both event, and tag props have been removed from <router-link>. You can use the v-slot API to fully customize <router-link>:
replace
<router-link to="/about" tag="span" event="dblclick">About Us</router-link>
with
<router-link to="/about" custom v-slot="{ navigate }">
<span @click="navigate" @keypress.enter="navigate" role="link">About Us</span>
</router-link>Reason: These props were often used together to use something different from an <a> tag but were introduced before the v-slot API and are not used enough to justify adding to the bundle size for everybody.
Removal of the exact prop in <router-link>
The exact prop has been removed because the caveat it was fixing is no longer present so you should be able to safely remove it. There are however two things you should be aware of:
- Routes are now active based on the route records they represent instead of the generated route location objects and their
path,query, andhashproperties - Only the
pathsection is matched,query, andhasharen't taken into account anymore
If you wish to customize this behavior, e.g. take into account the hash section, you should use the v-slot API to extend <router-link>.
Reason: See the RFC about active matching changes for more details.
Navigation guards in mixins are ignored
At the moment navigation guards in mixins are not supported. You can track its support at vue-router#454.
Removal of router.match and changes to router.resolve
Both router.match, and router.resolve have been merged together into router.resolve with a slightly different signature. Refer to the API for more details.
Reason: Uniting multiple methods that were used for the same purpose.
Removal of router.getMatchedComponents()
The method router.getMatchedComponents is now removed as matched components can be retrieved from router.currentRoute.value.matched:
router.currentRoute.value.matched.flatMap(record =>
Object.values(record.components)
)Reason: This method was only used during SSR and is a one liner that can be done by the user.
Redirect records cannot use special paths
Previously, a non documented feature allowed to set a redirect record to a special path like /events/:id and it would reuse an existing param id. This is no longer possible and there are two options:
- Using the name of the route without the param:
redirect: { name: 'events' }. Note this won't work if the param:idis optional - Using a function to recreate the new location based on the target:
redirect: to => ({ name: 'events', params: to.params })
Reason: This syntax was rarely used and another way of doing things that wasn't shorter enough compared to the versions above while introducing some complexity and making the router heavier.
All navigations are now always asynchronous
All navigations, including the first one, are now asynchronous, meaning that, if you use a transition, you may need to wait for the router to be ready before mounting the app:
app.use(router)
// Note: on Server Side, you need to manually push the initial location
router.isReady().then(() => app.mount('#app'))Otherwise there will be an initial transition as if you provided the appear prop to transition because the router displays its initial location (nothing) and then displays the first location.
Note that if you have navigation guards upon the initial navigation, you might not want to block the app render until they are resolved unless you are doing Server Side Rendering. In this scenario, not waiting the router to be ready to mount the app would yield the same result as in Vue 2.
Removal of router.app
router.app used to represent the last root component (Vue instance) that injected the router. Vue Router can now be safely used by multiple Vue applications at the same time. You can still add it when using the router:
app.use(router)
router.app = appYou can also extend the TypeScript definition of the Router interface to add the app property.
Reason: Vue 3 applications do not exist in Vue 2 and now we properly support multiple applications using the same Router instance, so having an app property would have been misleading because it would have been the application instead of the root instance.
Passing content to route components' <slot>
Before you could directly pass a template to be rendered by a route components' <slot> by nesting it under a <router-view> component:
<router-view>
<p>In Vue Router 3, I render inside the route component</p>
</router-view>Because of the introduction of the v-slot api for <router-view>, you must pass it to the <component> using the v-slot API:
<router-view v-slot="{ Component }">
<component :is="Component">
<p>In Vue Router 3, I render inside the route component</p>
</component>
</router-view>Removal of parent from route locations
The parent property has been removed from normalized route locations (this.$route and object returned by router.resolve). You can still access it via the matched array:
const parent = this.$route.matched[this.$route.matched.length - 2]Reason: Having parent and children creates unnecessary circular references while the properties could be retrieved already through matched.
Removal of pathToRegexpOptions
The pathToRegexpOptions and caseSensitive properties of route records have been replaced with sensitive and strict options for createRouter(). They can now also be directly passed when creating the router with createRouter(). Any other option specific to path-to-regexp has been removed as path-to-regexp is no longer used to parse paths.
Removal of unnamed parameters
Due to the removal of path-to-regexp, unnamed parameters are no longer supported:
/foo(/foo)?/suffixbecomes/foo/:_(foo)?/suffix/foo(foo)?becomes/foo:_(foo)?/foo/(.*)becomes/foo/:_(.*)
TIP
Note you can use any name instead of _ for the param. The point is to provide one.
Usage of history.state
Vue Router saves information on the history.state. If you have any code manually calling history.pushState(), you should likely avoid it or refactor it with a regular router.push() and a history.replaceState():
// replace
history.pushState(myState, '', url)
// with
await router.push(url)
history.replaceState({ ...history.state, ...myState }, '')Similarly, if you were calling history.replaceState() without preserving the current state, you will need to pass the current history.state:
// replace
history.replaceState({}, '', url)
// with
history.replaceState(history.state, '', url)Reason: We use the history state to save information about the navigation like the scroll position, previous location, etc.
routes option is required in options
The property routes is now required in options.
createRouter({ routes: [] })Reason: The router is designed to be created with routes even though you can add them later on. You need at least one route in most scenarios and this is written once per app in general.
Non existent named routes
Pushing or resolving a non existent named route throws an error:
// Oops, we made a typo in name
router.push({ name: 'homee' }) // throws
router.resolve({ name: 'homee' }) // throwsReason: Previously, the router would navigate to / but display nothing (instead of the home page). Throwing an error makes more sense because we cannot produce a valid URL to navigate to.
Missing required params on named routes
Pushing or resolving a named route without its required params will throw an error:
// given the following route:
const routes = [{ path: '/users/:id', name: 'user', component: UserDetails }]
// Missing the `id` param will fail
router.push({ name: 'user' })
router.resolve({ name: 'user' })Reason: Same as above.
Named children routes with an empty path no longer appends a slash
Given any nested named route with an empty path:
const routes = [
{
path: '/dashboard',
name: 'dashboard-parent',
component: DashboardParent,
children: [
{ path: '', name: 'dashboard', component: DashboardDefault },
{
path: 'settings',
name: 'dashboard-settings',
component: DashboardSettings,
},
],
},
]Navigating or resolving to the named route dashboard will now produce a URL without a trailing slash:
router.resolve({ name: 'dashboard' }).href // '/dashboard'This has an important side effect about children redirect records like these:
const routes = [
{
path: '/parent',
component: Parent,
children: [
// this would now redirect to `/home` instead of `/parent/home`
{ path: '', redirect: 'home' },
{ path: 'home', component: Home },
],
},
]Note this will work if path was /parent/ as the relative location home to /parent/ is indeed /parent/home but the relative location of home to /parent is /home.
Reason: This is to make trailing slash behavior consistent: by default all routes allow a trailing slash. It can be disabled by using the strict option and manually appending (or not) a slash to the routes.
$route properties Encoding
Decoded values in params, query, and hash are now consistent no matter where the navigation is initiated (older browsers will still produce unencoded path and fullPath). The initial navigation should yield the same results as in-app navigations.
Given any normalized route location:
- Values in
path,fullPathare not decoded anymore. They will appear as provided by the browser (most browsers provide them encoded). e.g. directly writing on the address barhttps://example.com/hello worldwill yield the encoded version:https://example.com/hello%20worldand bothpathandfullPathwill be/hello%20world. hashis now decoded, that way it can be copied over:router.push({ hash: $route.hash })and be used directly in scrollBehavior'seloption.- When using
push,resolve, andreplaceand providing astringlocation or apathproperty in an object, it must be encoded (like in the previous version). On the other hand,params,queryandhashmust be provided in its unencoded version. - The slash character (
/) is now properly decoded insideparamswhile still producing an encoded version on the URL:%2F.
Reason: This allows to easily copy existing properties of a location when calling router.push() and router.resolve(), and make the resulting route location consistent across browsers. router.push() is now idempotent, meaning that calling router.push(route.fullPath), router.push({ hash: route.hash }), router.push({ query: route.query }), and router.push({ params: route.params }) will not create extra encoding.
$router.push() and $router.replace() - onComplete and onAbort callbacks
Previously, $router.push() and $router.replace() accepted two callbacks, onComplete and onAbort, as second and third arguments. They were called after a navigation based on the result. With the introduction of a Promise based API, these callbacks are redundant and have been removed. See Navigation Failures for more information on how to detect successful and failed navigations.
Reason: Reduce library size by adapting to established JS standards (Promises).
TypeScript changes
To make typings more consistent and expressive, some types have been renamed:
vue-router@3 | vue-router@4 |
|---|---|
| RouteConfig | RouteRecordRaw |
| Location | RouteLocation |
| Route | RouteLocationNormalized |
New Features
Some of new features to keep an eye on in Vue Router 4 include:

