Vue Router for Vue 3
By Flavio Copes
Learn how to use Vue Router 5, the official routing library for Vue 3, to sync URLs with your views, install it, and map routes to components in your app.
Introduction
In a JavaScript web application, a router is the part that syncs the currently displayed view with the browser address bar content.
In other words, it’s the part that makes the URL change when you click something in the page, and helps to show the correct view when you hit a specific URL.
Traditionally the Web is built around URLs. When you hit a certain URL, a specific page is displayed.
With the introduction of applications that run inside the browser and change what the user sees, many applications broke this interaction, and you had to manually update the URL with the browser’s History API.
You need a router when you need to sync URLs to views in your app. It’s a very common need, and all the major modern frameworks now allow you to manage routing.
The Vue Router library is the way to go for Vue.js applications. Vue does not enforce the use of this library. You can use whatever generic routing library you want, or also create your own History API integration, but the benefit of using Vue Router is that it’s official.
This means it’s maintained by the same people who maintain Vue, so you get a more consistent integration in the framework, and the guarantee that it’s always going to be compatible in the future, no matter what.
This post covers Vue Router 5 for Vue 3 (createRouter, createWebHistory, RouterLink, RouterView). Vue 2 used Vue Router 3 with Vue.use(VueRouter) and new VueRouter(...) — that API is history now.
Installation
Vue Router is available via npm with the package named vue-router. Install the current 5.x release:
npm install vue-router
Or pin the major explicitly: npm install vue-router@5.
If you scaffold a new app with npm create vue@latest, you can enable the Router option and the project comes with Vue Router already wired.
If you load Vue from a CDN, you can also load Vue Router as an ESM module from unpkg (see the installation docs).
Once it’s installed, you create a router with createRouter and register it on the app with app.use(router):
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
// ...
]
})
createApp(App).use(router).mount('#app')
Prefer createWebHistory() for clean URLs. Use createWebHashHistory() if you need hash mode (#/about) without server fallback rules.
Inside components you can reach the router with:
useRouter()for navigation (push,replace,go)useRoute()for the current route
In Options API components you still get this.$router and this.$route after you call app.use(router).
The router object
The router object (from useRouter() or this.$router) offers many nice features.
We can make the app navigate to a new route using
router.push()router.replace()router.go()
which resemble the pushState, replaceState and go methods of the History API.
push() is used to go to a new route, adding a new item to the browser history. replace() is the same, except it does not push a new state to the history.
Usage samples with the Composition API:
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('about') //named route, see later
router.push({ path: 'about' })
router.push({ path: 'post', query: { post_slug: 'hello-world' } }) //using query parameters (post?post_slug=hello-world)
router.replace({ path: 'about' })
go() goes back and forth, accepting a number that can be positive or negative to go back in the history:
router.go(-1) //go back 1 step
router.go(1) //go forward 1 step
Defining the routes
I’m using a Vue Single File Component in this example.
In the template I use a nav tag that has 3 RouterLink components, which have a label (Home/Login/About) and a URL assigned through the to attribute.
The RouterView component is where the Vue Router will put the content that matches the current URL.
<template>
<div id="app">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/login">Login</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
<RouterView />
</div>
</template>
(router-link / router-view still work as the kebab-case names in templates.)
A RouterLink component renders an a tag by default (you can change that). Every time the route changes, either by clicking a link or by changing the URL, a router-link-active class is added to the element that refers to the active route, allowing you to style it.
In the JavaScript part we define 3 route components, pass them to createRouter, then mount the app with the router:
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
const Home = { template: '<div>Home</div>' }
const Login = { template: '<div>Login</div>' }
const About = { template: '<div>About</div>' }
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/about', component: About }
]
})
createApp({}).use(router).mount('#app')
Usually, in a Vue 3 app you instantiate and mount the root app using:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
When using Vue Router, you add .use(router) before mount:
createApp(App).use(router).mount('#app')
See in the example, we pass a routes array to createRouter. Each route in this array has a path and component params.
If you pass a name param too, you have a named route.
Using named routes to pass parameters to the router push and replace methods
Remember how we used the Router object to push a new state before?
router.push({ path: 'about' })
With a named route we can pass parameters to the new route:
router.push({ name: 'post', params: { post_slug: 'hello-world' } })
the same goes for replace():
router.replace({ name: 'post', params: { post_slug: 'hello-world' } })
What happens when a user clicks a RouterLink
The application will render the route component that matches the URL passed to the link.
The new route component that handles the URL is instantiated and its guards called, and the old route component will be destroyed.
Route guards
Since we mentioned guards, let’s introduce them.
You can think of them of life cycle hooks or middleware, those are functions called at specific times during the execution of the application. You can jump in and alter the execution of a route, redirecting or cancelling the request.
You can have global guards by adding a callback to the beforeEach() and afterEach() property of the router.
beforeEach()is called before the navigation is confirmedbeforeResolve()is called when beforeEach is executed and all the componentsbeforeRouterEnterandbeforeRouteUpdateguards are called, but before the navigation is confirmed. The final check, if you wantafterEach()is called after the navigation is confirmed
What does “the navigation is confirmed” mean? We’ll see it in a second. In the meantime think of it as “the app can go to that route”.
The usage is:
router.beforeEach((to, from) => {
// return false to cancel, or return a route location to redirect
})
router.afterEach((to, from) => {
// ...
})
to and from represent the route objects that we go to and from. In Vue Router 4+, you can return false to cancel navigation, or return a path/location object to redirect. The older next() callback still works if you prefer that style.
Single route components also have guards:
beforeRouteEnter(to, from)is called before the current route is confirmedbeforeRouteUpdate(to, from)is called when the route changes but the component that manages it is still the same (with dynamic routing, see next)beforeRouteLeave(to, from)is called when we move away from here
We mentioned navigation. To determine if the navigation to a route is confirmed, Vue Router performs some checks:
- it calls
beforeRouteLeaveguard in the current component(s) - it calls the router
beforeEach()guard - it calls the
beforeRouteUpdate()in any component that needs to be reused, if any exist - it calls the
beforeEnter()guard on the route object (I didn’t mention it but you can look here) - it calls the
beforeRouterEnter()in the component that we should enter into - it calls the router
beforeResolve()guard - if all was fine, the navigation is confirmed!
- it calls the router
afterEach()guard
You can use the route-specific guards (beforeRouteEnter and beforeRouteUpdate in case of dynamic routing) as life cycle hooks, so you can start data fetching requests for example.
Dynamic routing
The example above shows a different view based on the URL, handling the /, /login and /about routes.
A very common need is to handle dynamic routes, like having all posts under /post/, each with the slug name:
/post/first/post/another-post/post/hello-world
You can achieve this using a dynamic segment.
Those were static segments:
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/about', component: About }
]
})
we add in a dynamic segment to handle blog posts:
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/post/:post_slug', component: Post },
{ path: '/login', component: Login },
{ path: '/about', component: About }
]
})
Notice the :post_slug syntax. This means that you can use any string, and that will be mapped to the post_slug placeholder.
You’re not limited to this kind of syntax. Vue relies on this library to parse dynamic routes, and you can go wild with Regular Expressions.
Now inside the Post route component we can reference the route using useRoute(), and the post slug using route.params.post_slug:
import { useRoute } from 'vue-router'
const Post = {
setup() {
const route = useRoute()
return { route }
},
template: '<div>Post: {{ route.params.post_slug }}</div>'
}
We can use this parameter to load the contents from the backend.
You can have as many dynamic segments as you want, in the same URL:
/post/:author/:post_slug
Remember when before we talked about what happens when a user navigates to a new route?
In the case of dynamic routes, what happens is a little different.
Vue to be more efficient instead of destroying the current route component and re-instantiating it, it reuses the current instance.
When this happens, Vue calls the beforeRouteUpdate life cycle event. There you can perform any operation you need:
const Post = {
template: '<div>Post: {{ $route.params.post_slug }}</div>',
beforeRouteUpdate(to, from) {
console.log(`Updating slug from ${from.params.post_slug} to ${to.params.post_slug}`)
}
}
Using props
In the examples, I used route params to access the route data. A component should not be so tightly coupled with the router, and instead, we can use props:
const Post = {
props: ['post_slug'],
template: '<div>Post: {{ post_slug }}</div>'
}
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/post/:post_slug', component: Post, props: true }
]
})
Notice the props: true passed to the route object to enable this functionality.
Nested routes
Before I mentioned that you can have as many dynamic segments as you want, in the same URL, like:
/post/:author/:post_slug
So, say we have an Author component taking care of the first dynamic segment:
<template>
<div id="app">
<RouterView />
</div>
</template>
<script>
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
const Author = {
template: '<div>Author: {{ $route.params.author }}</div>'
}
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/post/:author', component: Author }
]
})
createApp({}).use(router).mount('#app')
</script>
We can insert a second RouterView component instance inside the Author template:
const Author = {
template: '<div>Author: {{ $route.params.author }}<RouterView /></div>'
}
we add the Post component:
const Post = {
template: '<div>Post: {{ $route.params.post_slug }}</div>'
}
and then we’ll inject the inner dynamic route in the router configuration:
const router = createRouter({
history: createWebHistory(),
routes: [{
path: '/post/:author',
component: Author,
children: [
{ path: ':post_slug', component: Post }
]
}]
})Want me to talk about your product? You can sponsor this site.