We call them SPAs (single page applications), but that's rarely the real case. Most of our applications are driven by multiple views and some kind of navigation. This is where Vue Routing comes in. It allows you to build the kinds of apps you need to create. Let's see how routing in Vue 3 works.

What is Routing?

In many Vue projects, you'd like the ability to navigate to different parts of your application. If you think about how websites traditionally work, you could navigate from the home page to other pages in the site. SPA navigation works like this. You have views in your Vue project and want to be able to navigate to individual views. As you can see in Figure 1, Routing allows your Single Page Application to define a section of the app to show individual views. As the URL changes, instead of a network request for other page, the SPA recognizes the change and changes the view.

This is accomplished differently in different frameworks, but the basic strategy is the same: The URL defines what content the SPA shows to the user. This also means that because the location inside your application is defined by the URL, that you can share these URLs to allow for deep linking into an application.

Figure 1: Anatomy of routing
Figure 1: Anatomy of routing

With that in mind, let's see how Vue implements routing.

Introducing Vue Routing

Routing in Vue is handled by a separate package from the core Vue package. It's entirely optional, but in many cases, you'll want to leverage the package. To get started, you'll need this package. You can install it with npm:

npm install vue-router --save

Now that it's installed, you can create your router.

Creating the Router

The purpose of the Router is to define a set of rules to map URL patterns to individual components. Assuming that you have at least one component, you can start creating your routes like so:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Cats from '@/views/Cats.vue'

const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/cats',
        name: 'Cats',
        component: Cats
    }
]

const router = createRouter({ history: createWebHistory(), routes })
export default router

To start out, you'll need to create an array of objects that describe each route. A route typically has a name, a path, and a destination. In this example, when the root path is specified, it loads a Home component. You then import the createRouter function from the vue-router package and use it to create a router object with the routes. This object is a singleton that you can just return from the file.

When creating the router, you need to specify a history mode. You do this with factory methods from vue-router. In this example, I'm using createWebHistory, which just uses browser history mode. Then the URLs just look like any other Web address:

http://localhost:8080/cats/catlist/5

This is likely what you'll want to use, but it does require your back-end to ignore URLs that are being handled by Vue. If you're just hosting this in a bare Web server (e.g., nginx), it just works. But if you're using a back-end like ASP.NET, JSP, etc., you'll need to configure them.

Alternatively, there is a createWebHashHistory factory method that uses the inter-page linking mechanism that doesn't affect back-end at all:

const router = createRouter({
    history: createWebHashHistory(), routes
})

This change is easier to work with but does affect the “good-looking” URLs from the Web History method (note the hash (#) in the URL):

http://localhost:8080/#cats/catlist/5

Our first use for the router is to add it to application (in main.js/ts usually):

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

You can see where you're passing into use when creating that app. You're effectively injecting it as a piece of middleware into the app so that you start watching for URL changes and following the route rules. But how do you use the router now that you have it?

Using the Router

Inside the vue-router package, there's a special component called RouterView. You can simply specify this component where you want it to exist in your UI (e.g., App.vue is common):

<!-- App.vue -->
<template>
    <div>
        <router-view/>
    </div>
</template>

The router view is used as a container where the component specified in the route will be loaded. As the URL changes, Vue replaces the view based on the Route rules. How do you specify changing the URL?

The next step is usually to add a component called a RouterLink. The router link creates an anchor tag that navigates to the new views. For example, you can add menu items to go to different views:

<div class="row">
    <div class="col-12">
        <div id="nav">
            <router-link to="/">Home</router-link> |
            <router-link to="/cats">Cats</router-link>
        </div>
    </div>

    <div class="col-12 bg-light">
        <router-view />
    </div>
</div>

In addition, you can make the links a bit less brittle by just specifying the route:

<router-link: to="{ name: 'Home' }">Home</router-link> | 
<router-link: to="{ name: 'Cats' }">Cats</router-link>

You can see here that you're binding the to attribute to an object that holds some properties (this will be more important soon). The name I'm using here is the same name that's specified in the router (above).

Because these links are generated as simple anchors, you can still style them:

<router-link :to="{ name: 'Home' }" class ="btn btn-info">Home</router-link>

Programmatic Navigation

Using RouterLinks is a common way to facilitate navigating from route to route, but there are occasions you want to navigate directly from code. You can do this directly in the template by using an injected object called $router:

<button @click="$router.back()" class="btn btn-primary btn-sm">&lt; Back</button>

The router object has several methods for navigating:

  • router.push (location, onComplete?, onAbout?): Navigates to a specific relative URL or named route. Optionally, it can specify callbacks for navigation completion or navigation abort:
router.push({ name: "Home" });
  • router.replace (location, onComplete?, onAbout?): The same as router.push but doesn't add another history entry.
router.replace({ name: "Home" });
  • router.go(n): Allows you to go forward or backward in the history of the app.
router.go(-2);
  • router.back(): Goes back one link in history. (Same as router.go(-1)).
router.back();

You can navigate from any code in a Vue project (e.g., Vuex, startup, utilities, etc.). To do this, just import the router object and use it like the injected $router object:

import router from "@/router";

export default 
{
    props: {
        count: { required: true },
    },
    setup(props) {
        function onBack() {
            console.log("Going Back...");
            router.back();
        }

Underneath the covers, this just modifies and reacts to the window.history APIs in the browser itself.

Route Parameters

Routes aren't limited to just names and simple URLs; you can have parameters in your routes too. For example, if I specify a route as:

{
    path: 'cat/:url',
    name: 'Cat',
    component: Cat
},

The URL segment that starts with a colon (i.e., :) is a dynamic route segment (or parameter). When linking to the route, you include the parameter like so:

<router-link:to="{  name: 'Cat', params: { url: c.url } }">

Note that with this syntax, you must specify a params object, not just the property of the parameter that you want. Alternatively, you could just construct the URL (although this means that your URLs are a bit more fragile):

<router-link:to="'/cat/' + c.url">

In the Cat component, you can access the parameters just referring to a $route object in the template:

<img src="$route.params.url" class="img-fluid img-thumbnail w-100" />

Another option here is to have the parameters passed to the component as props. To do this, your route needs to specify that it wants to opt into passing the values as parameters by setting props to true:

{
    name: "CatList",
    path: "catlist/:count",
    component: CatList,
    props: true
},

Then, in your component, you can just specify properties for the parameters that will be passed like so:

export default 
{
    props: 
    {
        count: { required: true }
    }
}

Then you can use it as just another bindable piece of data in your component:

<div>Count: {{ count }}</div>

Matching Routes

Sometimes you need to use regular expressions to match the routes you want. In the Vue Router, you can do that with pathMatch in a route. Instead of a simple route string, you can use a path with a parameter of pathMatch to match the route:

In this example, if the route matches anything, you're redirecting back to the root of your application. This only works because the routes are matched in order:

const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/cats',
        name: 'Cats',
        component: Cats
    },
    {
        path: '/:pathMatch(.*)*',
        redirect: "/"
    },
]

By making the catch-all route last, you can specify that if none of the routes are matched, you just send them to the home page (alternatively you can send them to a specific not-found component if you prefer). Although this is used for a catch-all mechanism in many apps, you can also use this to match any route by regular expression.

Nested Routing

So far, I've concentrated on top-level routing, but a common need is for nested routing. What exactly do I mean by nested routing? Much like the top-level routing that's so common, nested routing lets you define routes that are children of other routes. To specify these routes, you add them to a child property of the route itself:

{
    path: '/cats',
    name: 'Cats',
    component: Cats,
    children: [
        {
            name: "CatList",
            path: "catlist/:count",
            component: CatList,
            props: true
        },
        {
            path: 'cat/:url',
            name: 'Cat',
            component: Cat
        },
    ]
},

The path of the child routes are extensions to the path of the parent route. To see the catlist, the URL looks like:

http://localhost:8080/#/cats/catlist/5

If the route matches the top-level route (e.g., /cats) and the rest matches the child-route, it hosts the component in the views' own RouterView. This requires that you have a nested Router Viewer to hold the child views. If you look at Figure 2, you can see that the main RouterView object contains the views of whatever route you're on. Inside that, there's a child RouterView to hold child routes.

Figure 2: Nested router views
Figure 2: Nested router views

This allows you to have nested routes that are resolved on views and sub-views. Using nested routes can make debugging the route matching so use this only as necessary.

Routing Guards

The last important topic that I'll discuss is the idea of routing guards. Routing guards are a way to inject code that can continue or abort certain navigations. For example, if you want to write out navigations, you could set a handler for beforeEach:

const router=createRouter({
    history: createWebHashHistory(),
    routes
})
router.beforeEach((to, from, next) => {
    console.log(`Navigating to: ${to.name}`);
    next();
});

Each of the guards sends three parameters:

  • to: Route where the navigation is going
  • from: Route where the navigation is from
  • next: Function to call to continue the navigation

The beforeEach and afterEach can have handlers set and they're called for all route navigations. You can also have a handler on routes specifically (beforeEnter, afterEnter, beforeLeave, afterLeave):

{
    path: 'cat/:url',
    name: 'Cat',
    component: Cat,
    beforeEnter: (to, from, next) => 
    {
        next();
    }
},

The parameters here are identical but it only applies to navigation to and from this one specific route.

A common pattern is to create a guard forcing authentication. Although you could write a beforeEach handler to do this, a more common pattern is to create a function that performs the test and redirects to the log in if they aren't authenticated. For example:

function checkAuth(to, from, next) 
{
    if (IsAuthenticated()) next();
    else next("/login");
}

Notice that you can replace the navigation with another route by just supplying it to the next function. You can then just assign this to the beforeEnter of the routes that need it:

const routes = [
{
    path: '/',
    name: 'Home',
    component: Home
},
{
    path: '/cats',
    name: 'Cats',
    component: Cats,
    beforeEnter: checkAuth
},
    //...
]

This way, you can allow specific pages and require authentication (or any other requirement like roles or claims). Navigation Guards are a great way to control how the routing is used.

Where Are We?

Although I haven't covered every topic, you should now understand how to set up and use the Vue Router to create multiple view projects. It allows you to build more complex and mature SPAs without manipulating the Virtual DOM manually.