Vue Router Patterns
Routing is a vitally important part of anySingle Page Application(SPA). This chapter focuses on maximizing the Vue router and looks at everything from routing a user between pages, to parameters, to optimal configuration.
By the end of thischapter,we will have covered the following:
- Implementing routing in a Vue.js application
- Using dynamic route matching to create route parameters
- Passing route parameters as component props
Single Page Applications
Modern JavaScript applications implement a pattern known as an SPA. In its most simplistic form, it can be thought of as an application that displays components based on a URL. As the templates are mapped to routes, there is no need for a page reload, as they can be injected depending on where the user navigated.
This is the job of the router.
By creating our application this way, we're able to increase both perceived and actual speed, because our application is much more dynamic. If we add in the concepts that we learned in the previous chapter (HTTP), you'll find that they go hand in hand with the SPA model.
Using the router
Let's spin up a playground project and install thevue-routerlibrary. This allows us to take advantage of routing inside our application and give us the power of a modern SPA.
Run the following commands in your Terminal:
# Create a new Vue project
$ vue init webpack-simple vue-router-basics
# Navigate to directory
$ cd vue-router-basics
# Install dependencies
$ npm install
# Install Vue Router
$ npm install vue-router
# Run application
$ npm run dev
As we're using webpack as part of our build system, we've installed the router withnpm. We can then initialize the router inside ofsrc/main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
Vue.use(VueRouter);
new Vue({
el: '#app',
render: h => h(App)
});
This effectively registersVueRouteras a global plugin. A plugin simply is just a function that receivesVueandoptionsas parameters and allows libraries such asVueRouterto add functionality to our Vue application.
Creating routes
We can then define two small components inside ourmain.jsfile that simply have a template that showsh1with some text inside:
const Hello = { template: `
<
h1
>
Hello
<
/h1
>
` };
const World = { template: `
<
h1
>
World
<
/h1
>
`};
Then, in order to display these components on screen at particular URLs (such as/helloand/world), we can define routes inside our application:
const routes = [
{ path: '/hello', component: Hello },
{ path: '/world', component: World }
];
Now that we've defined what components we want to use as well as the routes inside of our application, we'll need to create a new instance ofVueRouterand pass along the routes.
Although we've usedVue.use(VueRouter), we still need to create a new instance ofVueRouterand initialize our routes. This is because merely registeringVueRouteras a plugin gives us access to the router option within our Vue instance(s):
const router = new VueRouter({
routes
});
We then need to pass therouterto our root Vue instance:
new Vue({
el: '#app',
router,
render: h =
>
h(App)
});
Finally, to display our routed components inside of ourApp.vuecomponent, we need to add therouter-viewcomponent inside thetemplate:
<
template
>
<
div id="app"
>
<
router-view/
>
<
/div
>
<
/template
>
If we then navigate to/#/hello/or/#/world, the appropriate component is displayed:
Dynamic routes
We can also dynamically match routes depending on a particular parameter. This is done by specifying a route with a colon before the parameter name. Here's an example using a similar greeting component:
// Components
const Hello = { template: `<h1>Hello</h1>` };
const HelloName = { template: `<h1>Hello {{ $route.params.name}}` }
// Routes
const routes = [
{ path: '/hello', component: Hello },
{ path: '/hello/:name', component: HelloName },
]
If our user navigates to/hello, they'll seeh1with the textHello. Otherwise, if they navigate to/hello/{name}(that is, Paul), they'll seeh1with the textHello Paul.
We've made a lot of progress, but it's important to know that when we navigate to parameterized URLs, component lifecycle hooks aren't fired again if the parameter changes (that is, from/hello/paulto/hello/katie). We'll look at this soon!
Route props
Let's change our/hello/nameroute to pass thenameparameter as acomponentprop, which can be done by adding theprops: trueflag to the route:
const routes = [
{ path: '/hello', component: Hello },
{ path: '/hello/:name', component: HelloName, props: true},
]
We can then update our component to take in a prop with anidof name and also log this to the console within the life cycle hook:
const HelloName = {
props: ['name'],
template: `<h1>Hello {{ name }}</h1>`,
created() {
console.log(`Hello ${this.name}`)
}
}
If we then try and navigate to different dynamic routes, we'll see that the created hook only fires once (unless we refresh the page) even though our page shows the correct name:
Component Navigation Guards
How do we fix the lifecycle hook problem? In this instance, we can use what's known as a Navigation Guard. This allows us to hook into different lifecycles of the router, such as thebeforeRouteEnter,beforeRouteUpdate, andbeforeRouteLeavemethods.
beforeRouteUpdate
Let's use thebeforeRouteUpdatemethod to access information about the route change:
const HelloName = {
props: ['name'],
template: `<h1>Hello {{ name }}</h1>`,
beforeRouteUpdate(to, from, next) {
console.log(to);
console.log(from);
console.log(`Hello ${to.params.name}`)
},
}
If we check the JavaScript console after navigating to a different route under/hello/{name}, we'll be able to see which route the user is going to and where they are coming from. Thetoandfromobjects also give us access toparams, queries, the full path, and much more.
While we correctly get the log statements, if we try and navigate between routes, you'll note that our application doesn't update with the parameternameprop. This is because we haven't used thenextfunction after we've finished doing any computations within the guard. Let's add that in:
beforeRouteUpdate(to, from, next) {
console.log(to);
console.log(from);
console.log(`Hello ${to.params.name}`)
next();
},
beforeRouteEnter
We can also take advantage ofbeforeRouteEnterto perform actions prior to entering the component route. Here's an example:
beforeRouteEnter(to, from, next) {
console.log(`I'm called before entering the route!`)
next();
}
We still have to callnextto pass the stack down to the next route handler.
beforeRouteLeave
We can also hook intobeforeRouteLeaveto perform actions whenever we're navigating away from a route. As we're already on this route within the context of this hook, we have access to the component instance. Let's look at an example:
beforeRouteLeave(to, from, next) {
console.log(`I'm called before leaving the route!`)
console.log(`I have access to the component instance, here's proof!
Name: ${this.name}`);
next();
}
Once again, we have to callnextin this instance.
Global router hooks
We've looked at component Navigation Guards and while these work on a component-by-component basis, you may want to establish global hooks that listen to navigation events.
beforeEach
We can userouter.beforeEachto listen for routing events globally across the application. This is worth using if you have authentication checks or other pieces of functionality that should be used in every route.
Here's an example that simply logs out the route the user is going to and coming from. Each one of the following examples assume that the router exists in scope similar to the following:
const router = new VueRouter({
routes
})
router.beforeEach((to, from, next) => {
console.log(`Route to`, to)
console.log(`Route from`, from)
next();
});
Once again, we have to callnext()to trigger the next route guard.
beforeResolve
ThebeforeResolveglobal route guard is triggered just before navigation is confirmed, but it's important to know that this is only after all component-specific guards and async components have been resolved.
Here's an example:
router.beforeResolve((to, from, next) => {
console.log(`Before resolve:`)
console.log(`Route to`, to)
console.log(`Route from`, from)
next();
});
afterEach
We can also hook into the globalafterEachfunction that allows us to perform the action(s), but we can't affect navigation and thus only have access to thetoandfromparameters:
router.afterEach((to, from) => {
console.log(`After each:`)
console.log(`Route to`, to)
console.log(`Route from`, from)
});
Resolution stack
Now that we've familiarized ourselves with the various different route lifecycle hooks on offer, it's worth investigating the entire resolution stack whenever we attempt to navigate to another route:
Trigger a route change
:
This is the first stage of any route lifecycle and is triggered any time we
attempt
to navigate to a new route. An example would be going from
/hello/Paul
to
/hello/Katie
. No Navigation Guards have been triggered at this point.Trigger component leave guards
:
Next, any leave guards are triggered, such as
beforeRouteLeave
,
on loaded components.Trigger global beforeEach guards
:
As global route middleware can be created with
beforeEach
, these functions will be called prior to any route update.Trigger local beforeRouteUpdate
guards in reused components:
As we saw earlier, whenever we navigate to the same route with a different parameter, the lifecycle hooks aren't fired twice. Instead, we use
beforeRouteUpdate
to trigger lifecycle changes.Trigger beforeRouteEnter in components
:
This is called each time prior to navigating to any route. At this stage, the component isn't rendered, so it doesn't have access to the
this
component instance.Resolve asynchronous route components
:
It then attempts to resolve any asynchronous components in your project. Here's an example of one:
const MyAsyncComponent = () => ({
component: import ('./LazyComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 150,
timeout: 3000
})
Trigger beforeRouteEnter in successfully activated components
:We now have access to the
beforeRouteEnter
hook and can perform any action(s) prior to resolving the route.Trigger global beforeResolve hooks
:
Providing in-component guards and async route components have been resolved, we can now hook into the global
router.beforeResolve
method that allows us to perform action(s) at this stage.Navigation
:
All prior Navigation Guards have been fired, and the user is now successfully navigated to a route.
Trigger afterEach hooks
:
Although the user has been navigated to the route, it doesn't stop there. Next, the router triggers a global
afterEach
hook that has access to the
to
and
from
parameters.
As the route has already been resolved at this stage, it doesn't have the next parameter and thus cannot affect navigation.Trigger DOM updates
:
Routes have been resolved, and Vue can appropriately trigger DOM updates.
Trigger callbacks within next in beforeRouteEnter
:
As
beforeRouteEnter
does not have access to the component's
this
context, the
next
parameter takes a callback that resolves to the component instance on navigation. An example can be seen here:
beforeRouteEnter (to, from, next) {
next(comp => {
// 'comp' inside this closure is equal to the component instance
})
Programmatic navigation
We're not limited to template navigation usingrouter-link; we can also programmatically navigate the user to different routes from within our JavaScript. Inside of ourApp.vue, let's expose the<router-view>and give the user the ability to select a button that will navigate them to either the/helloor/hello/:nameroute:
<template>
<div id="app">
<nav>
<button @click="navigateToRoute('/hello')">/Hello</button>
<button
@click="navigateToRoute('/hello/Paul')">/Hello/Name</button>
</nav>
<router-view></router-view>
</div>
</template>
We can then add a method that pushes a new route onto the route stack:
<script>
export default {
methods: {
navigateToRoute(routeName) {
this.$router.push({ path: routeName });
},
},
};
</script>
At this point, any time we select a button, it should subsequently navigate the user to the appropriate route. The$router.push()function can take a variety of different arguments, depending on how you have your routes set up. Here are some examples:
// Navigate with string literal
this.$router.push('hello')
// Navigate with object options
this.$router.push({ path: 'hello' })
// Add parameters
this.$router.push({ name: 'hello', params: { name: 'Paul' }})
// Using query parameters /hello?name=paul
this.$router.push({ path: 'hello', query: { name: 'Paul' }})
router.replace
Instead of pushing a navigation item on the stack, we can also replace the current history stack withrouter.replace. Here's an example of this:
this.$router.replace({ path: routeName });
router.go
If we want to navigate the user backward or forward, we can userouter.go; this is essentially an abstraction over thewindow.historyAPI. Let's take a look at some examples:
// Navigate forward one record
this.$router.go(1);
// Navigate backward one record
this.$router.go(-1);
// Navigate forward three records
this.$router.go(3);
// Navigate backward three records
this.$router.go(-3);
Lazy loading routes
We can also lazy load our routes to take advantage of code splitting with webpack. This allows us to have greater performance than when eagerly loading our routes. To do this, we can create a small playground project. Run the following in your Terminal:
# Create a new Vue project
$ vue init webpack-simple vue-lazy-loading
# Navigate to directory
$ cd vue-lazy-loading
# Install dependencies
$ npm install
# Install Vue Router
$ npm install vue-router
# Run application
$ npm run dev
Let's start off by creating two components, namedHello.vueandWorld.vue, insidesrc/components:
// Hello.vue
<template>
<div>
<h1>Hello</h1>
<router-link to="/world">Next</router-link>
</div>
</template>
<script>
export default {};
</script>
Now we have created ourHello.vuecomponent, let's create the secondWorld.vuelike so:
// World.vue
<template>
<div>
<h1>World</h1>
<router-link to="/hello">Back</router-link>
</div>
</template>
<script>
export default {};
</script>
We can then initialize our router as we usually do, insidemain.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
The main difference has to do with the way in which to import our components. This requires the use of thesyntax-dynamic-importBabel plugin.Install it into your project by running the following in your Terminal:
$ npm install --save-dev babel-plugin-syntax-dynamic-import
We can then update.babelrcto use the new plugin:
{
"presets": [["env", { "modules": false }], "stage-3"],
"plugins": ["syntax-dynamic-import"]
}
Finally, this allows us to import our components asynchronously, like this:
const Hello = () =
>
import('./components/Hello');
const World = () =
>
import('./components/World');
We can then define our routes and initialize the router, this time referencing the asynchronous import:
const routes = [
{ path: '/', redirect: '/hello' },
{ path: '/hello', component: Hello },
{ path: '/World', component: World },
];
const router = new VueRouter({
routes,
});
new Vue({
el: '#app',
router,
render: h => h(App),
});
We can then see its results by looking in Chrome viaDeveloper Tools|Network tabwhile navigating through our application:
Each route is added to its own bundle file and subsequently gives us improved performance as the initial bundle is much smaller:
An SPA project
Let's create a project that uses a RESTful API and the routing concepts that we've just learned. Create a new project by running the following in your Terminal:
# Create a new Vue project
$ vue init webpack-simple vue-spa
# Navigate to directory
$ cd vue-spa
# Install dependencies
$ npm install
# Install Vue Router and Axios
$ npm install vue-router axios
# Run application
$ npm run dev
Enabling the router
We can start off by enabling theVueRouterplugin within our application. To do this, we can create a new file insidesrc/routernamedindex.js. We'll use this file to contain all the router-specific configuration, but we'll separateouteach route into different files depending on the underlying feature.
Let's import and add the router plugin:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter)