Use the following command:
npm install vue-router@4 --save
It is done in the main.js
or main.ts
file.
The routes can be imported from an external file outside of main.js
.
In JavaScript, a route is defined like this:
routes: [
//routes go here
{
//the path is the url when you navigate in the browser
path: '/',
//the name of the route, which is a more handy way to use <router-link> component
name: 'Home',
//the component to load for the route
component: HomeVue
},
However, in TypeScript, you need to do it slightly differently:
-
You create a route variable:
const UserLoginRoute: RouteRecordRaw = { path: RoutePath.UserLogin, name: RouteName.UserLogin, component: () => import("@/pages/UserLogin.vue"), };
-
You create the
RouterOptions
where you initialze theroutes
array and add the route variable:const routerOptions: RouterOptions = { history: createWebHistory(import.meta.env.BASE_URL), routes: [UserLoginRoute], };
-
You create the router:
const router: Router = createRouter(routerOptions);
To render the view for a route, you need to include in the App.vue
the component router-view
.
<template>
<RouterView></RouterView>
</template>
or
<template>
<router-view></router-view>
</template>
It is recommended to include the :key
prop to make sure that the components are rendered on route change.
If you want to build a navigation, using the router-link
component enables to create adequate links:
<template>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
<router-view></router-view>
</template>
Using router-link
instead of a
elements makes possible to load just what we need instead of everything on each pageload.
If you open the DevTools, the router-link
is rendered as a
element in the DOM.
But vue-router intercepts the click events so the browser doesn't reload the page.
Therefore, we'll use a
elements for external links and router-link
for internal links.
This is commun practice. You create a router
directory under src
where you add index.js
with the setup of the router.
import { createRouter, createWebHistory } from "vue-router";
import HomeVue from "../views/Home.vue";
import AboutVue from "../views/About.vue";
const routes = [
//routes go here
{ path: "/", name: "Home", component: HomeVue },
{ path: "/about", name: "About", component: AboutVue },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
What is the difference?
The #
mode is there to imitate a file path without the server setup. It is used in SPAs.
Without the #
, it uses the HTML5 native web history feature.
The drawback of #
mode has a bad impact on SEO, so prefer not using it for public applications (but SPAs are not good for SEO either, BTW. See Nuxt framework).
So in Vue applications, use createWebHistory
, which is the HTML5 mode.
Without lazy loading, all the JavaScript modules of all routes will be loaded. It is not efficient.
To lazy load routes, you need to use code splitting of the bundles generated by the builder ; in my case, I use Vite.
Without lazy loading, you define a route this way:
import BrazilVue from '@/views/Brazil.vue';
var routes = [
{
path: '/brazil',
name: 'brazil',
component: BrazilVue'),
}
]
The result is in the dist
folder, you have a JavaScript file (index-[hash].js
).
With lazy loading, you define a route with a dynamic import:
var routes = [
{
path: "/brazil",
name: "brazil",
component: () => import("@/views/Brazil.vue"),
},
];
The result is in the dist
folder:
- the main JavaScript file (
index-[hash].js
) - a JavaScript file for each component of a given route.
With Vuecli, it uses a similar technique but using a Webpack comment to name the chunk with magic comment:
var routes = [
{
path: "/brazil",
name: "brazil",
component: () =>
import(/* webpackChunkName: "brazil" */ "@/views/Brazil.vue"),
},
];
It is easy to do that with the linkActiveClass
property in the createRouter
options.
You can add parameters to a route using :myparam
.
This parameter is accessible in the route object, under params
object.
The params
object contains properties whose names are the name of the parameters specified in the route's path.
As for the naming of the parameters, use camelCase
, PascalCase
or snake_case
.
kebab-case
will not work.
So:
myparam
will be available asroute.params.myparam
,myParam
will be available asroute.params.myParam
,my_param
will be available asroute.params.my_param
,my-param
will NOT be available inroute.params
.
The best practice: I'd use Douglas Crockford's suggestions that say (in bold what I think applies to naming route's params):
Names should be formed from the 26 upper and lower case letters (A .. Z, a .. z), the 10 digits (0 .. 9), and _ (underbar). Avoid use of international characters because they may not read well or be understood everywhere. Do not use $ (dollar sign) or \ (backslash) in names.
Do not use _ (underbar) as the first character of a name. It is sometimes used to indicate privacy, but it does not actually provide privacy. If privacy is important, use the forms that provide private members. Avoid conventions that demonstrate a lack of competence.
Most variables and functions should start with a lower case letter.
Conclusion: I'll use camelCase
only.
Here for some discussion on the topic:
It is recommended to give all routes a name to use it in the code.
That will allow to change the path easily.
If you use router-link
component with a route name that doesn't exist, you will get console warning:
TheNavigation.vue:4 [Vue warn]: Property "doesntexist" was accessed during render but is not defined on instance.
at <TheNavigation>
at <App>
Interesting fact: if the route name equals undefined
though, you will get no warning. But the a
element will have href
equal to the currently loaded page.
I wonder what is the best practice to use named routes within a component to avoid typos.
Personally, in TypeScript, I use a RouteName
enum that I use not only in the router index but also in the various places where I need to create a router link.
That technique allow intellisence and guarantee that you never mistype a route name.
Here is an example:
export enum RouteName {
//Public pages
TheHome = "TheHome",
UserShow = "UserShow",
CategoryShow = "CategoryShow",
ForumShow = "ForumShow",
ThreadShow = "ThreadShow",
ThreadCreate = "ThreadCreate",
ThreadEdit = "ThreadEdit",
UserRegister = "UserRegister",
UserLogin = "UserLogin",
UserLogout = "UserLogout",
//Behind auth pages
AccountEdit = "AccountEdit",
AccountShow = "AccountShow",
//Commun page
NotAuthorized = "NotAuthorized",
NotFound = "NotFound",
}
I do the same of the route paths BTW.
We have two options:
-
use
watch
to listen on therouter.params
changes and fetch the new data. -
ask Vue to recreate the component using the
key
attribut on therouter-view
component. The value is$route.path
, but be aware of the query string... You might need something like this:<router-view :key="`${$route.path}${JSON.stringify($route.query)}`"> </router-view>
The second approach takes more JavaScript computation time.
But it is the solution to use most of the time as you use the same component for different data.
To build loosely-built component, passing props to routes make the code easier to maintain.
For example, a route would be defined at follows:
const routes = [
{
path: "/destination-details/:id/:slug",
name: routesNames.destinationShow,
component: () => import("@/views/DestinationShow.vue"),
props: (route) => ({ slug: route.params.slug }),
},
];
It would be used the following way in the component:
<script setup>
import useSourceData from "@/composables/useSourceData";
const props = defineProps({
slug: {
type: String,
required: true,
},
});
const { destination } = useSourceData(props.slug);
</script>
This way, the component can receive the props from any source: route, form input, etc...
When you have an application with components nested multiple levels deep, it is very commun that a certain route segment corresponds to the matching component.
For example, a DestinationDetails
component containing a list of ExperienceDetails
component will be configured as follows:
- the destination route could be
/destination-details/:id/:slug
- the experience route for a destination would be
:experienceSlug
and would be nested as a child route.
const routes = [
{ path: "/", name: "home", component: HomeVue },
{
//parent route
path: "/destination-details/:id/:slug",
name: routesNames.destinationShow,
component: () => import("@/views/DestinationShow.vue"),
props: (route) => ({ id: parseInt(route.params.id) }),
children: [
{
//child route with only the extra segment needed.
path: ":experienceSlug",
name: "experience-details",
component: () => import("@/views/ExperienceShow.vue"),
props: (route) => ({
id: parseInt(route.params.id),
experienceSlug: route.params.experienceSlug,
}),
},
],
},
];
The functionnality is available out-of-the-box with the instance of $router
. It defines back()
that acts exactly like the back button of a browser.
The component <transition>...</transition>
is available in Vue2 by wrapping the <router-view>...</router-view>
component.
<transition>
<router-view>...</router-view>
</transition>
In Vue3, the syntax was changed: router-view
wraps the transition
by using the router view slot.
Then, the transition component contains an anonymous component.
<router-link v-slot="{ Component }">
<transition name="slide">
<component :is="Component" :key="$route.path"></component>
</transition>
</router-link>
A key attribut equal to $route.path
is mandatory on the anonymous component to insure the transition occurs properly and that the route changes continue to destroy and recreate the components to load.
IMPORTANT: for the transition component to apply the proper CSS classes on each page, all pages must have a single root element.
Otherwise, you will get a blank page and this warning in the console:
[Vue warn]: Component inside <Transition> renders non-element root node that cannot be animated.
You simply declare a route catch all
using the Vue 3 syntax (more flexible and performant):
const routes = [
{
path: "/:pathMatch(.*)*",
name: "notfound",
component: NotFoundVue,
},
];
In Vue 2, it was simplier to understand but gave us less flexibility:
const routes = [
{
path: "*",
name: "notfound",
component: NotFoundVue,
},
];
They allow to hook into the navigation process and perform checks (before we navigate to the requested route or before we navigate away from the current route) and act accordingly: continue or doing something else.
For example, we define a route:
const routes = [
//routes go here
{ path: "/", name: "home", component: HomeVue },
{
path: "/destination-details/:id/:slug",
name: routesNames.destinationShow,
component: () => import("@/views/DestinationShow.vue"),
},
];
The URL /destination-details/1/france
could serve data, but maybe /destination-details/100/france
won't. If the data doesn't exist, it won't be caught by the catch all route.
That's when navigation guards are useful.
We define it on the appropriate event. In the example, before we enter the next route:
beforeEnter(to, from, next) {
//to is the destination route
//from is the origin route
//so we want to check the data contains the element matching route.params.id
const exists = sourceData.destinations.find(
(element) => element.id === parseInt(to.params.id),
);
if (!exists) return next({ name: 'notfound' });
//otherwise we continue
next();
},
While the above guard is an example of a guard in the route definition, also called middleware, you use the guards in the components.
Usually, you use navigation guards defined in the route definition when it is auth-related and if you are making sure the requested data is available.
In some case, you may need to componentless route with a guard that runs some store action: a sign out route is one of those usecases.
const UserLogoutRoute: RouteRecordRaw = {
path: "/logout",
name: RouteName.UserLogout,
redirect: "",
beforeEnter: async (_to, _from, next) => {
await logoutUser();
updateFetching();
next({
name: RouteName.TheHome,
});
},
};
If you want to keep the URL intact even if it is wrong, you will need to add the following to return statement:
return {
name: "notfound",
//allows keeping the URL intact while rendering a different page
params: { pathMatch: to.path.split("/").slice(1) },
query: to.query,
hash: to.hash,
};
The other two guards are beforeLeave
and beforeUpdate
.
The common usecase for beforeLeave
is to catch the scenario when the user fills a form and click away. You can show the user a Wanna save? modal thanks to this guard.
You may notice that when you change route and if you scroll down, the scroll position is saved.
To use the native behavior of non-SPA application, you will need to implement scrollBehavior guard:
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
//this restore the top position with 300 ms dely to avoid a visual bug since we have a transition active.
return (
savedPosition ||
new Promise((resolve) => {
setTimeout(() => resolve({ top: 0, behavior: "smooth" }), 300);
})
);
},
});
The above applies to all routes. But what if you want to apply it conditionally?
Using meta
on a given route, you can execute the scroll to top only if the route has some meta data set.
For example:
- You define the route:
{
path: "/account/edit",
name: "UserConnectedEdit",
component: () => import("@/pages/UserShow.vue"),
meta: { toTop: true, smoothScroll: true },
}
-
You define the behavior in the
createRouter
options:scrollBehavior(to) { //this restore the top position with 300 ms dely to avoid a visual bug since we have a transition active. const scroll = {}; if (to.meta.toTop) scroll.top = 0; if (to.meta.smoothScroll) scroll.behavior = "smooth"; return new Promise((resolve) => { setTimeout(() => resolve(scroll), 500); }); },
They are abitrary information passed along the route.
A common use case for route meta fields is to mark some routes as protected.
Using a application-wide guard or global route guard, in the router definition, we can deal with the authentication requirement as follows, in a global route guard:
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !user) {
// Load a login page
return {
name: "login",
};
}
//Else continue
});
Using the push method on the router instance on the login page, for example, we can redirect the user to the protected area and log him out:
-
for the login:
//script to handle the login (FAKE) import { ref } from "vue"; import { useRouter } from "vue-router"; const router = useRouter(); const username = ref(""); const password = ref(""); const loginUser = () => { window.userLogged = `{user: ${username.value}, password: ${password.value}}`; router.push({ name: "protected" }); };
-
for the the logout:
//script of the protected area page import { useRouter } from "vue-router"; const router = useRouter(); //data of user to display in the view const username = window.user; const logoutUser = () => { window.user = undefined; router.push({ name: "home" }); };
NB: meta fields are passed on to the children in the case of nested routes.
To read query parameters, it is as simple as reading the query
object in the route
.
An example usecase would be to include the path to a destination page after authentication.
For example, I have an invoices page that requires authentication. If I navigate to it directly, I'd want to return to the invoices page after I sgin-in.
To do so, we need to modify the global route guard:
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !window.userLogged) {
// Load a login page
return {
name: "login",
//add the query string "redirect" with the requested path
query: { redirect: to.fullPath },
};
}
//else we continue
});
Again, to navigate to external links, we don't use router-link
but a regular a
element.
However, it is a good idea to use a single application-web component for both router-link
and regular a
element.
We could then create an AppLink.vue
(to follow the style guidelines).
It would look like this:
<template>
<a
v-if="isExternal"
:href="to"
target="_blank"
rel="noopener"
class="external-link"
>
<slot></slot>
</a>
<router-link v-else v-bind="$props" class="internal-link"
><slot></slot
></router-link>
</template>
<script setup>
import { computed, guardReactiveProps } from 'vue';
import { RouterLink } from 'vue-router';
const props = defineProps({
...RouterLink.props
});
const isExternal = computed(() => {
return typeof props.to === "string" && props.to.startsWith('http');
});
</script>
And its usage would be:
<AppLink :to="{name: 'dashboard'}">Dashboard</AppLink>
<AppLink to="https://google.com">Google</AppLink>
The method push
will add a new entry in the history stack, while replace
won't.
replace
effectively replaces the entry or remove it from the history stack.
The usecase is customization of layouts per route.
For example, the following defines a LeftSideBar
component used in the Dashboard
route of the routes definition array:
{
path: '/dashboard',
name: 'dashboard',
components: {
default: () => import('@/views/Dashboard.vue'),
LeftSideBar: () => import('@/components/LeftSideBar.vue'),
},
meta: {
requiresAuth: true,
},
},
You can then add the router-view
as follow in App.vue
to make available in the application:
<router-view class="view left-sidebar" name="LefSideBar"></router-view>
The name
attribut is important: the value must match the key in the components
object of the route using the component.
Redirects allow to catch a route and redirect it another:
{ path: '/', name: 'home', component: HomeVue },
{ path: '/home', redirect: { name: 'home' } },
The URL changes in a redirect.
Aliases load the target view, but the URL doesn't change.
{ path: '/', name: 'home', component: HomeVue, alias: '/home' },
Why would it happen?
- Users are already on the page that they are trying to navigate to.
- A guard aborts the navigation by returning false
- A new navigation guard takes place before the previous one has finished
- A navigation guard redirects somewhere else by returning a new location
- A navigation guard throw an Error.
To trigger the cases, see the following case:
import { NavigationFailureType, isNavigationFailure, useRouter } from "vue-router";
const router = useRouter();
const triggerRouterError = async () => {
//let navigate to the home page while on the home page...
const navigationResult = await router.push('/');
console.log(navigationResult);
if (isNavigationFailure(navigationResult, NavigationFailureType.duplicated)) {
//the route is the current and cannot be navigated to
}
else if (isNavigationFailure(navigationResult, NavigationFailureType.aborted)) {
//false is returned in a navigation guard
}
else if (isNavigationFailure(navigationResult, NavigationFailureType.cancelled)) {
//a new navigation took place before the current could finish.
}
else {
//all was fine.
}
navigationResult
contains the to
and from
objects and the typical route properties.
Using regular expressions on a route's path can allow fine tuning of the route.
For example:
{
//match a route ending with at least 1 digit and nothing else.
// => "/example/1" will work
// => "/example/123" will work
// => "/example/one" won't work and will load the 404 page
path: '/example/:id(\\d+)',
component: () => import('@/views/Example.vue'),
},
This can be extended to having a list of data:
{
//match a route ending with at least 1 digit and nothing else.
// => "/example/1/2/3/4" will provide in params a value "id" being an array of STRING values 1, 2, 3 and 4.
path: '/example/:id+',
component: () => import('@/views/Example.vue'),
},
Having the +
makes requires to provide at least one value for id
. Otherwise, you get a 404.
Having the *
makes the id
optional and it will load the route and its component. route.params.id
will just be undefined
.
And put it together with format restriction and repeatable params:
{
//match a route ending with at least 1 digit and nothing else.
// => "/example/1/2/3/4" will provide in params a value "id" being an array of INTEGER values 1, 2, 3 and 4.
path: '/example/:id(\\d+)+',
component: () => import('@/views/Example.vue'),
},
Finally, making the parameter optional without making it repeatable, use ?
:
{
//match a route ending with 0 to N digits and nothing else.
// => "/example" will work
// => "/example/123" will work
path: '/example/:id(\\d+)?',
component: () => import('@/views/Example.vue'),
},
Using the addRoute
method of the router
instance, you add a route on the fly:
const addDynamicRoute = () => {
router.addRoute({
name: "dynamic",
path: "/dynamic",
component: () => import("@/views/UserLogin.vue"),
});
};
The caveat is: you cannot use the name of the route until you are sure it is available.
So using the path:
<button @click="addDynamicRoute" class="btn">Add dynamic route</button>
<!-- will show and lead to 404 if the button isn't clicked -->
<router-link to="/dynamic">Visite Dynamic Route</router-link>
<!-- will break the current page since the named route "dynamic" doesn't exist -->
<router-link :to="{name: 'dynamic'}">Visite Dynamic Route</router-link
Similarly, you can remove routes:
router.removeRoute("dynamic");