A Beginner’s Guide to Vue Router
In this tutorial, we’ll be looking at how we can implement routing in a Vue app using Vue Router. So we can have a hands-on practice, we’ll be building a simple Pokedex app using Vue and Vue Router.
Specifically, we’ll be covering the following:
- setting up a router
- route parameters
- declarative and programmatic navigation
- nested routes
- 404 pages
Every JavaScript UI framework that allows the creation of single page applications needs a way to navigate users from one page to another. All of this needs to be managed on the client-side by syncing the view that’s currently displayed on the page with the URL in the address bar. In the Vue world, the [official library] for managing this type of task is Vue Router.
As ever, the code for this tutorial can be found on GitHub.
Prerequisites
The following are required so you can make the best use of this tutorial:
- Basic knowledge of HTML, CSS, JavaScript, and Vue. If you know how to render something on the page using Vue, you should be able to follow along. A little knowledge on APIs would also help.
- Node.js and Vue CLI installed on your machine. We’ll be using Vue 3 in this tutorial so make sure Vue CLI is updated.
App Overview
We’re going to build a Pokedex app. It will have three pages:
-
Pokemon list page. This is the default page which lists all the original 151 Pokemon.
-
Pokemon page. This is where we display the basic details such as type and the description.
-
Pokemon details page. This is where we display the evolution chain, abilities and moves.
Setting Up the App
Spin up a new Vue app using the Vue CLI:
vue create poke-vue-router
Choose Vue 3 from the options listed:
Once it’s done, navigate inside the project folder and install the libraries we need:
cd poke-vue-router
npm install vue-router@4 axios
Note that we’re using Vue Router 4 instead of 3, which is the default result that shows up when you Google it. It’s at next.router.vuejs.org
as opposed to router.vuejs.org
. We’re using Axios to make a request to PokeAPI v2.
At this point, it’s a good idea to run the project to make sure the default Vue app is working:
npm run serve
Visit http://localhost:8080/
on your browser and check to see if the default Vue app is running. It should show something like this:
Next, you need to add sass-loader
as a dev dependency. For the purpose of this tutorial, it’s best to just install the same version I used. This is because, at the time of writing, the latest version isn’t compatible with Vue 3:
npm install sass-loader@10.1.1 --save-dev
You also need to install node-sass
, for the same reason as above. It’s best to stick with the same version as mine:
npm install node-sass@4.14.1 --save
Note: if installing Sass this way doesn’t work for you, you can also choose Manually select features when creating the Vue app with the CLI. Then, select CSS Preprocessors and pick Sass/SCSS (with dart-sass).
Creating the App
Now we’re ready to start building the app. As you follow along, remember that the root directory is the src
folder.
Start by updating the main.js
file. This is where we import the root component App.vue
and the router/index.js
file where we declare all things related to routing:
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");
Setting up a router
In the App.vue
file, use the router-view
component provided by Vue Router. This is the top-most component used by Vue Router that renders the corresponding component for the current path visited by the user:
// App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
};
</script>
Next, create a new router/index.js
file and add the following. To create a router, we need to extract createRouter
and createWebHistory
from Vue Router. createRouter
allows us to create a new router instance, while createWebHistory
creates an HTML5 history that’s basically a wrapper for the History API. It allows Vue Router to manipulate the address in the address bar when we’re navigating between pages:
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
Below that, import all the pages we’ll be using:
import PokemonList from "../views/PokemonList.vue";
Vue Router requires an array of objects containing the path
, name
, and component
as its properties:
path
: this is the pattern you’d like to match. In the code below, we’re matching for the root path. So if the user tries to accesshttp://localhost:8000
, this pattern is matched.name
: the name of the page. This is the unique identifier for the page and is what’s being used when you want to navigate to this page from other pages.component
: the component you want to render when thepath
matches the URL the user accessed.
const routes = [
{
path: "/",
name: "PokemonList",
component: PokemonList,
},
];
Finally, create the router instance by supplying an object containing the history
and the routes
to createRouter
:
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
That’s all we need for now. You might be wondering where the other pages are. We’ll add them later as we go along. For now, let’s work on the default page first.
Creating a page
Creating a page doesn’t really need any special code. So if you know how to create a custom component in Vue, you should be able to create a page for Vue Router to use.
Create a views/PokemonList.vue
file and add the code below. In this file, we’re using a custom List
component to render the Pokemon list. The only thing we really need to do is to supply the data for the List
component to use. We make a request to PokeAPI once the component is mounted. We don’t want the list to get too big, so we’re limiting the results to the original 151 Pokemon. Once we get the results back, we simply assign it to the component’s items
data. This will in turn update the List
component:
<template>
<List :items="items" />
</template>
<script>
import axios from "axios";
import List from "../components/List.vue";
export default {
name: "PokemonList",
data() {
return {
items: null,
};
},
mounted() {
axios.get(`https://pokeapi.co/api/v2/pokemon?limit=151`).then((res) => {
if (res.data && res.data.results) {
this.items = res.data.results;
}
});
},
components: {
List,
},
};
</script>
Here’s the code for the List
component. Components are stored in the components
directory, so create a components/List.vue
file and add the following:
<template>
<div v-if="items">
<router-link
:to="{ name: 'Pokemon', params: { name: row.name } }"
class="link"
v-for="row in items"
:key="row.name"
>
<div class="list-item">
{{ row.name }}
</div>
</router-link>
</div>
</template>
<script>
export default {
name: "List",
props: {
items: {
type: Array,
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/list.scss";
</style>
You can check out the code for the styles/list.scss
file in the GitHub repo.
At this point, you can now view the changes in the browser. Except you get the following error instead:
This is because Vue is trying to generate the link to the Pokemon page but there isn’t one yet. The Vue CLI is smart enough to warn you of that. You can temporarily solve this issue by using a <div>
instead for the template of components/List.vue
file:
<template>
<div v-if="items">
<div v-for="row in items" :key="row.name">{{ row.name }}</div>
</div>
</template>
With that, you should be able to see the list of Pokemon. Remember to change this back later once we add the Pokemon page.
Declarative Navigation
With Vue Router, you can navigate in two ways: declaratively, and programmatically. Declarative navigation is pretty much the same as what we do with the anchor tag in HTML. You just declare where you want the link to navigate to. On the other hand, programmatic navigation is done by explicitly calling Vue Router to navigate to a specific page when a user action is performed (such as a button button being clicked).
Let’s quickly break down how this works. To navigate, you need to use the router-link
component. The only property this requires is :to
. This is an object containing the name
of the page you want to navigate to, and an optional params
object for specifying the parameters you want to pass to the page. In this case, we’re passing in the name of the Pokemon:
<router-link
:to="{ name: 'Pokemon', params: { name: row.name } }"
class="link"
v-for="row in items"
:key="row.name"
>
<div class="list-item">
{{ row.name }}
</div>
</router-link>
To visualize how this works, you need to know the pattern used by the Pokemon
screen. Here’s what it looks like: /pokemon/:name
. :name
represents the param name
that you passed in. For example, if the user wanted to view Pikachu, the URL would look like http://localhost:8000/pokemon/pikachu
. We’ll get back to this in more detail shortly.
Route parameters
We’ve already seen how we can match specific patterns for our routes, but we haven’t gone through how we can pass in custom parameters yet. We’ve seen it briefly through the router-link
example earlier.
We’ll use the next page (Pokemon
) to illustrate how route parameters work in Vue Router. To do that, all you need to do is prefix the name of the parameter with colon (:
). In the example below, we want to pass in the name of the Pokemon, so we added :name
. This means that if we want to navigate to this specific route, we need to pass in a value for this parameter. As we’ve seen in the router-link
example earlier, this is where we pass the name of the Pokemon:
// router/index.js
import PokemonList from "../views/PokemonList.vue";
import Pokemon from "../views/Pokemon"; // add this
const routes = [
{
path: "/",
name: "PokemonList",
component: PokemonList,
},
// add this:
{
path: "/pokemon/:name",
name: "Pokemon",
component: Pokemon,
}
]
Here’s the code for the Pokemon
page (views/Pokemon.vue
). Just like the PokemonList page earlier, we’re delegating the task of rendering the UI to a separate component BasicDetails
. When the component is mounted, we make a request to the API’s /pokemon
endpoint. To get the Pokemon name passed in as a route parameter, we use this.$route.params.name
. The property we’re accessing should be the same as the name you gave to the parameter in the router/index.js
file. In this case, it’s name
. If you used /pokemon/:pokemon_name
for the path
instead, you access it with this.$route.params.pokemon_name
:
<template>
<BasicDetails :pokemon="pokemon" />
</template>
<script>
import axios from "axios";
import BasicDetails from "../components/BasicDetails.vue";
export default {
name: "Pokemon",
data() {
return {
pokemon: null,
};
},
mounted() {
const pokemon_name = this.$route.params.name;
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokemon_name}`)
.then((res) => {
const data = res.data;
axios
.get(`https://pokeapi.co/api/v2/pokemon-species/${pokemon_name}`)
.then((res) => {
Object.assign(data, {
description: res.data.flavor_text_entries[0].flavor_text,
specie_id: res.data.evolution_chain.url.split("/")[6],
});
this.pokemon = data;
});
});
},
components: {
BasicDetails,
},
};
</script>
Here’s the code for the BasicDetails
component (components/BasicDetails.vue
):
<template>
<div v-if="pokemon">
<img :src="pokemon.sprites.front_default" :alt="pokemon.name" />
<h1>{{ pokemon.name }}</h1>
<div class="types">
<div
class="type-box"
v-for="row in pokemon.types"
:key="row.slot"
v-bind:class="row.type.name.toLowerCase()"
>
{{ row.type.name }}
</div>
</div>
<div class="description">
{{ pokemon.description }}
</div>
<a @click="moreDetails" class="link">More Details</a>
</div>
</template>
<script>
export default {
name: "BasicDetails",
props: {
pokemon: {
type: Object,
},
},
methods: {
moreDetails() {
this.$router.push({
name: "PokemonDetails",
params: {
name: this.pokemon.name,
specie_id: this.pokemon.specie_id,
},
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/types.scss";
@import "../styles/pokemon.scss";
</style>
You can check out the code for the styles/types.scss
and styles/pokemon.scss
file in the GitHub repo.
At this point, you should be able to see the changes in the browser again. You can also update the components/List.vue
file back to its original code with the router-link
on it instead of the <div>
.
Programmatic Navigation
You might have noticed that we’ve done something different in the BasicDetails
component. We didn’t really navigate to the PokemonDetails
page using router-link
. Instead, we used an anchor element and intercepted its click event. This is how programmatic navigation is implemented. We can get access to the router via this.$router
. Then we call the push()
method to push a new page on top of the history stack. Whatever page is on top will be displayed by the router. This method allows for navigating back to the previous page when the user clicks on the browser’s back button, since clicking it simply “pops” the current page on top of the history stack. This method accepts an object containing the name
and params
properties, so it’s pretty much the same thing you pass to the to
property in the router-link
:
methods: {
moreDetails() {
this.$router.push({
name: "PokemonDetails",
params: {
name: this.pokemon.name,
specie_id: this.pokemon.specie_id,
},
});
},
},
Nested routes
Next, update the router file to include the path for the Pokemon details page. Here, we’re using nested routes to pass in more than one custom parameter. In this case, we’re passing in the name
and specie_id
:
import Pokemon from "../views/Pokemon";
import PokemonDetails from "../views/PokemonDetails"; // add this
const routes = [
// ..
{
path: "/pokemon/:name",
// ..
},
// add these
{
path: "/pokemon/:name/:specie_id/details",
name: "PokemonDetails",
component: PokemonDetails,
},
];
Here’s the code for the PokemonDetails
page (views/PokemonDetails.vue
):
<template>
<MoreDetails :pokemon="pokemon" />
</template>
<script>
import axios from "axios";
import MoreDetails from "../components/MoreDetails.vue";
export default {
name: "PokemonDetails",
data() {
return {
pokemon: null,
};
},
mounted() {
const pokemon_name = this.$route.params.name;
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokemon_name}`)
.then((res) => {
const data = res.data;
axios.get(`https://pokeapi.co/api/v2/evolution-chain/${this.$route.params.specie_id}`)
.then((res) => {
let evolution_chain = [res.data.chain.species.name];
if (res.data.chain.evolves_to.length > 0) {
evolution_chain.push(
res.data.chain.evolves_to[0].species.name
);
if (res.data.chain.evolves_to.length > 1) {
const evolutions = res.data.chain.evolves_to.map((item) => {
return item.species.name;
}
);
evolution_chain[1] = evolutions.join(" | ");
}
if (
res.data.chain.evolves_to[0].evolves_to.length >
0
) {
evolution_chain.push(res.data.chain.evolves_to[0].evolves_to[0].species.name);
}
Object.assign(data, {
evolution_chain,
});
}
this.pokemon = data;
});
});
},
components: {
MoreDetails,
},
};
</script>
Here’s the code for the MoreDetails
components (components/MoreDetails.vue
):
<template>
<div v-if="pokemon">
<h1>{{ pokemon.name }}</h1>
<div v-if="pokemon.evolution_chain" class="section">
<h2>Evolution Chain</h2>
<span v-for="(name, index) in pokemon.evolution_chain" :key="name">
<span v-if="index">-></span>
{{ name }}
</span>
</div>
<div v-if="pokemon.abilities" class="section">
<h2>Abilities</h2>
<div v-for="row in pokemon.abilities" :key="row.ability.name">
{{ row.ability.name }}
</div>
</div>
<div v-if="pokemon.moves" class="section">
<h2>Moves</h2>
<div v-for="row in pokemon.moves" :key="row.move.name">
{{ row.move.name }}
</div>
</div>
</div>
</template>
<script>
export default {
name: "MoreDetails",
props: {
pokemon: {
type: Object,
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/more-details.scss";
</style>
You can view the contents of the styles/more-details.scss
file on the GitHub repo.
At this point you can click on any of the Pokemon names and view the details of an individual Pokemon. You might need to restart the server to see the changes.
404 page
We’ve added the code for all of the pages. But what happens if the user enters an invalid URL to the browser’s address bar? In those cases, it will simply error out or not display anything at all. We need to add a way to intercept those requests so we can display a “404 not found” page.
To do that, open the router file and import the NotFound
page:
import NotFound from "../views/NotFound";
Routes are prioritized based on the order they’re added in the routes array. This means that the ones added first are the first ones being matched with the URL entered by the user on the address bar. So the pattern for the 404 page has to be added last.
In the routes
array, add the following:
const routes = [
// ..
{
path: "/pokemon/:name/:specie_id/details",
// ..
},
// add this
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: NotFound,
},
];
Does the path
look familiar? We’re using a custom param named pathMatch
to match for whatever URL is entered. So if the user entered http://localhost:8000/hey
or http://localhost:8000/hey/jude
, it would render the NotFound
page.
This is all well and good. But what happens if the patterns above the catch-all pattern are actually matched? For example:
http://localhost:8000/pokemon/someinvalidpokemon
http://localhost:8000/pokemon/someinvalidpokemon/99999/details
In these cases, the catch-all pattern wouldn’t match, so we need a way to intercept such requests.
The main issue with those kinds of requests is that the user is assuming that a certain Pokemon or species ID exists, but it doesn’t. The only way to check is to have a list of valid Pokemon. In your routes file, import the list of valid Pokemon:
import NotFound from "../views/NotFound";
import valid_pokemon from "../data/valid-pokemon.json"; // add this
You can find this file on the GitHub repo.
To intercept these kinds of requests, Vue Router provides navigation guards. Think of them as “hooks” to the navigation process that allow you to execute certain actions before or after Vue Router has navigated to a certain page. We’ll only be going through the one executed before the navigation is done, as this allows us to redirect to a different page if our condition for navigating to that page isn’t matched.
To hook into the current request before the navigation is done, we call the beforeEach()
method on the router
instance:
const router = createRouter({
// ..
});
router.beforeEach(async (to) => {
// next: add the condition for navigating to the 404 page
});
Vue Router passes two arguments to it:
to
: the target route locationfrom
: the current route location
Each one contains these properties. What we’re interested in is the params, as this contains whatever params the user has passed in the URL.
Here’s what our condition looks like. We first check whether the params we want to check exists. If it does, we proceed to check if it’s valid. The first condition matches for the Pokemon
page. We use the valid_pokemon
array from earlier. We compare it with to.params.name
, which contains the name of the Pokemon passed by the user. On the other hand, the second condition matches for the PokemonDetails
page. Here we’re checking for the species ID. As we only want to match the original 101 Pokemon, any ID that’s greater than that is considered invalid. If it matches any of these conditions, we simply return the path to the 404 page. If the conditions didn’t match, it will navigate to where its originally meant to navigate to:
if (
to.params &&
to.params.name &&
valid_pokemon.indexOf(to.params.name) === -1
) {
return "/404";
}
if (
(to.params &&
to.params.name &&
to.params.specie_id &&
valid_pokemon.indexOf(to.params.name) === -1 &&
to.params.specie_id < 0) ||
to.params.specie_id > 101
) {
return "/404";
}
Here’s the code for the 404 page (views/NotFound.vue
):
<template>
<h1>404 Not Found</h1>
</template>
<script>
export default {
name: "Not Found",
};
</script>
<style lang="scss" scoped>
@import "../styles/notfound.scss";
</style>
You can view the code for the styles/notfound.scss
file on the GitHub repo.
At this point, the app is complete! You can try visiting invalid pages and it will return a 404 page.
Conclusion
That’s it! In this tutorial, you learned the basics of using Vue Router. Things like setting up a router, passing custom parameters, navigating between pages, and implementing a 404 page will bring you a long way. If you want some direction on where to go from here, I recommend exploring the following topics:
- Passing props to route components: allows you to decouple your view components from the route params. This provides a way to swap the route params with props that can be accessed from the component. That way you can use your components anywhere which doesn’t have
$route.params
. - Transitions: for animating the transition between pages.
- Lazy loading: this is more of a performance improvement so the bundler doesn’t compile the codes for all the pages in a single file. Instead, it will lazy load it so that the browser only downloads the code for a specific page once it’s needed.
FAQs ABout Vue Router
Vue Router is the official routing library for Vue.js. It enables navigation between different views of a Vue.js application, allowing you to create single-page applications (SPAs) with multiple components.
You can install Vue Router using npm or yarn. For npm, you can run npm install vue-router
. For yarn, use yarn add vue-router
. After installation, you need to import it into your project and use it in your main Vue instance.
A route in Vue Router represents a mapping between a URL and the content to be displayed. Routes are defined in the router configuration, and they specify which component should be rendered when a particular URL is accessed.
Routes are typically defined in the router configuration using an array of route objects. Each route object specifies a path (URL) and the component to be rendered when that path is accessed.
Yes, Vue Router allows you to define dynamic segments in your route paths, and these segments can be used as parameters. You can access these parameters in your component using $route.params
.
Vue Router allows you to define a catch-all route (*
) or use the router.resolve()
method to handle 404 scenarios and redirect or display a custom not-found component.
A: Yes, Vue Router provides navigation guards such as beforeEach
that allow you to control access to routes. This is useful for implementing authentication and authorization logic before rendering a component.