Router

Published on Sep 11, 2022

Vue.js Router is a client-side router - new pages are loaded without the need of server "trips".

How it works

The router is loaded and added to the app in main.js:

src/main.js

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

createApp(App).use(store).use(router).mount("#app");

The routes are defined in router/index.js:

src/router/index.js

import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

const routes = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  {
    path: "/about",
    name: "about",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

To create a link to the page:

<router-link to="/">Home</router-link>

Or, using a standard HTML anchor tag:

<a href="/">Home</a>

Or, pushing the new route on a button click:

<button @click="$router.push('/')">Home</button>

Or, inside a component method:

src/views/ProjectView.vue

<template>
  <div>
    <h1>Project</h1>
    <p>ID: {{ $route.params.id }}</p>
    <button @click="nextProject">Back</button>
  </div>
</template>

<script>
export default {
  name: "ProjectView",
  methods: {
    goBack() {
      this.$router.push({
        name: "project",
        params: {
          id: this.$route.params.id + 1, 
          // 😱 don't do this in a real project
        },
      });
    },
  },
};
</script>

There are several ways to specify the page to push onto the router:

src/views/ProjectView.vue


// absolute path as string
this.$router.push('/project/1'); 

// absolute path as object
this.$router.push({ path: '/project/1' }); 

// named route 
this.$router.push({ name: 'project', params: { id: 1 } }); 

// with query, resulting in /projects?search=vuejs
this.$router.push({ path: '/projects', query: { search: 'vuejs' } }); 
  • to - a string, an object, or a function that returns a string or an object
  • replace - boolean, if true, the navigation will not leave a history record
  • active-class - string, the class to apply when the link is active
  • exact - boolean, if true, the active class will only be applied if the route is matched exactly
  • event - string, the event to listen to for triggering the link

By default, the router-link component will add the router-link-active class to the element when the link is active. You can customize the active class by passing the active-class prop:

<router-link to="/" active-class="active">Home</router-link>

<!-- or, using the "exact" prop -->
<router-link to="/" exact active-class="active">Home</router-link>

When navigating via push, one pushes a new entry onto the history stack, a default behaviour in web browsers. Here are 2 more ways to navigate:

// replace the current entry on the history stack
this.$router.replace('/project/1');

// navigate forward or backward in the history stack
this.$router.go(1);
this.$router.go(-1);

Parameters

Dynamic Routes

Route parameters are defined in the route definition:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "project",
    component: ProjectView,
  },
  ...
];
...

And accessed in the component:

src/views/ProjectView.vue

<template>
  <div>
    <h1>Project</h1>
    <p>ID: {{ $route.params.id }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectView",
};
</script>

To link to that page:

<router-link :to={ name: 'ProjectView', params: { id: '42'} } >Project 42</router-link>

The id can be set as a parameter for the component:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "project",
    props: true,
    component: ProjectView,
  },
  ...
];
...

And accessed in the component:

src/views/ProjectView.vue

<template>
  <div>
    <h1>Project</h1>
    <p>ID: {{ id }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectView",
  props: ["id"],
};
</script>

Query parameters

Query parameters (eg. /projects?search=vue) can be accessed in the component via the $route.query syntax:

src/views/ProjectsView.vue

<template>
  <div>
    <h1>Projects</h1>
    <p>Search: {{ $route.query.search }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectsView",
};
</script>

To generate a link to a page with a parameter:

<router-link :to="{ name: 'ProjectsView', 
  query: { search: 'vue' } }">Search for Vue projects</router-link>

The search can be set as a parameter for the component:

src/router/index.js

...

const routes = [
  ...
  {
    path: "/projects",
    name: "projects",
    props: (route) => ({ search: route.query.search }),
    component: ProjectsView,
  },
  ...
];
...

And accessed in the component:

src/views/ProjectsView.vue

<template>
  <div>
    <h1>Projects</h1>
    <p>Search: {{ search }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectsView",
  props: ["search"],
};
</script>

Transform Query Parameters

Query parameters can be transformed using the props option - this can be as simple as a name change:

src/router/index.js

...

const routes = [
  ...
  {
    path: "/projects",
    name: "projects",
    props: (route) => ({ searchTerm: route.query.search }),
    component: ProjectsView,
  },
  ...
];
...

And accessed in the component:

src/views/ProjectsView.vue

<template>
  <div>
    <h1>Projects</h1>
    <p>Search: {{ search }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectsView",
  props: ["searchTerm"],
};
</script>

Or more complex, given the syntax above is a shorthand for an anonymous function :

src/router/index.js

...

const routes = [
  ...
  {
    path: "/projects",
    name: "projects",
    props: (route) => {
      const searchTerm = route.query.search || '';
      const page = parseInt(route.query.page) || 1;
      return { searchTerm, page };
    },
    component: ProjectsView,
  },
  ...
];
...

Nested Routes

Children routes

Routes can be nested, especially whilst building CRUD. For example, a ProjectShow and a ProjectEdit component will share a lot of functionality (eg: page layout, API calls) so a common pattern is to use a "layout" component, which can be used to render a common layout for all child routes:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "ProjectLayout",
    props: true,
    component: ProjectLayout,
    children: [
      {
        path: '',  // default child route
        name: "ProjectShow",
        component: ProjectShow,
      },
      {
        path: "edit", // child route "/projects/:id/edit"
        name: "ProjectEdit",
        component: ProjectEdit,
      },
    ],
  },
  ...
];
...

The children routes can be accessed via:

<router-link :to="{ name: 'ProjectShow', params: { id: 42 } }">
  Project 1</router-link>
<router-link :to="{ name: 'ProjectEdit', params: { id: 42 } }">
  Project 1</router-link>

NB: In the example above, he id props requirement for the "parent", is also a requirement for the ProjectShow and ProjectEdit child components.

"Layout" component

The ProjectLayout component will render the common layout for all child routes:

src/views/projects/project_layout.vue

<template>
  <div id="nav">
    <h1>Project: {{project.title}}</h1>
    <router-link :to="{ name: 'ProjectShow', params: { id } }">
      Project Details</router-link>
    <router-link :to="{ name: 'ProjectEdit', params: { id } }">
      Project Edit</router-link>
  </div>
  <div id="view">
    <router-view :project="project"></router-view>
  </div>
</template>

<script>
export default {
  name: "ProjectLayout",
  props: ["id"],
  ...
};
</script>

router-view is where the child component will be rendered. The child view will receive the project prop from the parent:

src/views/projects/project_show.vue

<template>
  <div>
    <h2>Title: {{project.title}}</h2>
    <p>Objectives: {{project.objectives}}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectShow",
  props: ["project"],
};
</script>

Aliasses

Path redirect

Redirect an old path (now obsolete) to a newer path. For example /project/:id to the REST-full path, /projects/:id :

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "ProjectLayout",
    ...
  },
  {
    path: "/project/:id",
    redirect: { name: "ProjectLayout" },
  },
  ...
];
...

We'll also need to redirect any children present:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "ProjectLayout",
    component: ProjectLayout,
    children: [
      {
        path: "",
        name: "ProjectShow",
        component: ProjectShow,
      },
      {
        path: "edit",
        name: "ProjectEdit",
        component: ProjectEdit,
      },
    ],
  },
  {
    path: "/project/:id",
    redirect: { name: "ProjectLayout" },
    children: [
      {
        path: "edit",
        redirect: { name: "ProjectEdit" },
      },
    ],
  },
  ...
];
...

Same result can be achieved using afterEvent:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/project/:afterEvent(.*)",
    redirect: to => {
      return { name: "ProjectLayout", params: to.params.afterEvent };
    }
  },
  ...
];
...

Router Alias

A simple alias does the same job:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "ProjectLayout",
    ...,
    alias: "/project/:id",
  },
  ...
];
...

There is a slight difference however - the alias will not redirect so might not be ideal if SEO is important.

Meta Route information

Route meta information can be used to store additional information about a route. For example, we can use it to store the title of the page or if a page requires authentication - it's simply a JS object that can contain any property:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/projects/:id",
    name: "ProjectLayout",
    component: ProjectLayout,
    meta: { title: "Project" },
    children: [
      {
        path: "",
        name: "ProjectShow",
        component: ProjectShow,
        meta: { title: "Project Details" },
      },
      {
        path: "edit",
        name: "ProjectEdit",
        component: ProjectEdit,
        meta: { requiresAuth: true, title: "Project Edit" },
      },
    ],
  },
  ...
];
...

Now we can access the meta information from the route object:

src/router/index.js

...
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

router.beforeEach((to, from, next) => {
  document.title = to.meta.title;
  if(to.meta.requiresAuth) {
    // check if user is authenticated
    if(notAuthenticated) {
      // set a flash message
      // redirect with login 
      if(from.href) {
        next({ name: "Login", query: { redirect: from.href } });
        // alternatively, we can stop the navigation
        // next(false);
      } else {
        next({ name: "Login" });
      }
    }
  }
  next();
});
...

NB: Meta information is inherited by the children routes.

404 Errors

There are two ways to handle 404: using a catchAll route, or a beforeEach guard

Using a catchAll route is the simplest way to handle 404 - we'll define a NotFound component and use it as the last entry in the routes array:

src/router/index.js

...
import NotFound from "../views/errors/NotFound.vue";
...
const routes = [
  ...
  {
    path: "/:catchAll(.*)",
    name: "NotFound",
    component: NotFound,
  },
];
...

Using a beforeEach guard is a more flexible way to handle 404. We can use it to redirect to a different route, or to a different component:

src/router/index.js

...
import NotFound from "../views/errors/NotFound.vue";
...
const router = createRouter({
  ...
  routes,
});
...
router.beforeEach((to, from, next) => {
  if (to.matched.length === 0) {
    next({ name: "NotFound" });
  } else {
    next();
  }
});
...

Router guards

Router guards can be used to prevent the user from accessing a page. For example, if the user is not logged in, they should not be able to access the user page.

Router guards are defined in the router:

src/router/index.js

...
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  // router guard
  beforeEnter: (to, from, next) => {
    // check if user is logged in
    if (to.name === "user" && !store.state.user) {
      // redirect to login page
      next({ name: "login" });
    } else {
      // continue to route
      next();
    }
  },
});
...

And in the component:

src/views/ProjectView.vue

<template>
  <div>
    <h1>User</h1>
    <p>User ID: {{ $route.params.id }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectView",
  // component guard
  beforeRouteEnter(to, from, next) {
    // check if user is logged in
    if (to.name === "user" && !store.state.user) {
      // redirect to login page
      next({ name: "login" });
    } else {
      // continue to route
      next();
    }
  },
};
</script>

Router guards can also be used to check if the user is allowed to access a page. For example, if the user is not an admin, they should not be able to access the admin page.

Navigation guards

Navigation guards can be used to prevent the user from navigating to another page. For example, if the user has unsaved changes, they should be asked if they want to leave the page.

Navigation guards are defined in the router:

src/router/index.js

...
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  // router guard
  beforeEnter: (to, from, next) => {
    // check if user is logged in
    if (to.name === "user" && !store.state.user) {
      // redirect to login page
      next({ name: "login" });
    } else {
      // continue to route
      next();
    }
  },
  // navigation guard
  beforeEach(to, from, next) {
    // check if user has unsaved changes
    if (store.state.unsavedChanges) {
      // ask user if they want to leave the page
      if (confirm("You have unsaved changes. Are you sure you want to leave?")) {
        // continue to route
        next();
      } else {
        // stay on the current page
        next(false);
      }
    } else {
      // continue to route
      next();
    }
  },
});
...

And in the component:

src/views/ProjectView.vue

<template>
  <div>
    <h1>User</h1>
    <p>User ID: {{ $route.params.id }}</p>
  </div>
</template>

<script>
export default {
  name: "ProjectView",
  // component guard
  beforeRouteEnter(to, from, next) {
    // check if user is logged in
    if (to.name === "user" && !store.state.user) {
      // redirect to login page
      next({ name: "login" });
    } else {
      // continue to route
      next();
    }
  },
  // navigation guard
  beforeRouteLeave(to, from, next) {
    // check if user has unsaved changes
    if (store.state.unsavedChanges) {
      // ask user if they want to leave the page
      if (confirm("You have unsaved changes. Are you sure you want to leave?")) {
        // continue to route
        next();
      } else {
        // stay on the current page
        next(false);
      }
    } else {
      // continue to route
      next();
    }
  },
};
</script>

Performance enhancements

Lazy Loading

Lazy loading of routes can be used to improve performance. This is done by using the component property in the route definition:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/about",
    name: "about",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
  },
  ...
];
...

When the route is visited, the component is loaded and added to the page.

Multiple components can be grouped into the same package by using the same webpackChunkName:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/about",
    name: "about",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
  },
  {
    path: "/contact",
    name: "contact",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/ContactView.vue"),
  },
  ...
];
...

Both AboutView and ContactView will be loaded in the same package, about.

keep-alive

Another performance enhancement is to use the keep-alive directive on the <router-view> element. This will keep the component in memory, so that it is not reloaded when the route is visited again.

src/App.vue

<template>
  <div id="app">
    <header>
      <nav>
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
      </nav>
    </header>
    <main>
      <keep-alive>
        <router-view></router-view>
      </keep-alive>
    </main>
  </div>
</template>

Preloading

When using the router, the components are loaded on demand. This means that the first time a component is loaded, it will be loaded from the server. To avoid this, the components can be preloaded:

src/router/index.js

...
const routes = [
  ...
  {
    path: "/user/:id",
    name: "user",
    component: () => import(/* webpackPrefetch: true */ "../views/ProjectView.vue"),
  },
  ...
];
...

When the user hovers over the link, the component will be loaded in the background.

Misc

Scroll

src/router/index.js

...
const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition;
    } else {
      return { x: 0, y: 0 };
    }
  }
});
...