Routing in Vue.js — Starting Guide
Tags: javascript, vue, webdev
Published: 2020-09-22 18:28:29

With the advent of web applications, it became necessary to modify URLs using JavaScript. **History API** of the browser came to the rescue.
Because of this, all major modern frameworks allow you to programmatically control routing that synchronizes the URL with the application view.
For routing in Vue applications, you can create your own integration with the History API, but it’s better to use the official Vue library — Vue-Router

# Basic things
You can start using it even with a CDN installation:
```html
<script src="https://unpkg.com/vue-router"></script>
```
But we’ll start right away with the “correct” option — Vue Cli:

```bash
yarn global add @vue/cli
# OR
npm i -g @vue/cli
```

Let’s create a project using the VUE CLI with a basic template — Default ([Vue 2] babel, eslint):
```bash
vue create vue-router-test-app
```

# Minimal configuration
Add a router:
```bash
yarn add vue-router
# OR
npm i --save vue-router
```
Let’s add the minimal router configuration to **/src/main.js**:
```javascript
import Vue from "vue";
import App from "@/App.vue";
import VueRouter from "vue-router";
import HelloWorld from "@/components/HelloWorld";

Vue.use(VueRouter);

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
];

const router = new VueRouter({
  routes,
  mode: "history",
});

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");
```
Routes are an array, each element of which is an object where you need to specify `path` and `component`.
To see the changes, you need to display the router component — `routerView` which is responsible for displaying. To do this, let's change `/src/App.vue`:

```html
<template>
  <div id="app">
    <router-view /> 
  </div>
</template>
```

Now, go to **http://localhost:8080/**. We will see a page with a “/” route, where the `HelloWorld.vue` component is displayed, instead of the tag `router-view` that we wrote in `App.vue`.

# Path hierarchy

Let’s add a route to `main.js` (array routes):

```javascript
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page"]),
    },
  },
];
```
Let’s go to the address **http://localhost:8080/board**. We will see the second page displaying the render function.

# Route Props

Let’s fix the child route for the “/” board route in main.js. For child components, you need to specify where in the parent component to display the child components `router-view`. In our case, this is in the render function:

```bash

import Board from "@/components/Board";
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    children: [
      {
        path: '/board/:id',
        component: Board,
      }
    ]
  },
];
```

Let me remind you that the render function in the template view will look like this:

```html
<template>
  <div>
    Board Page
    <router-view />
  </div>
</template>
```

Let’s create a `/src/components/Board.vue` component with content:

```vue
<template>
  <div>Board with prop id: {{ id }}</div>
</template>
<script>
export default {
  computed: {
    id() {
      return this.$route.params.id;
    },
  },
};
</script>
```
Let’s go to the address **http://localhost: 8080/board/21** and see the parent and child components `Board` passing the parameter `id` equal to 21.


Route parameters are available in the by component `this.$route.params`.


If we want to more explicitly display the dependence of the component on the input parameters, we use the setting `props: true` when configuring the route:

```javascript
children: [
  {
    path: '/board/:id',
    component: Board,
    props: true,
  }
]
```
And in the `/src/components/Board.vue` component, accept idas an input parameter of the component:

```vue
<template>
  <div>Board with prop id: {{ id }}</div>
</template>
<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
};
</script>
```

# Route meta

```javascript

const routes = [
    {
      path: "",
      component: HelloWorld,
      meta: {
        dataInMeta: "test",
      },
    },
     ....
   ]
```

We can now access the route metadata from the `HelloWorld.vue` component as follows: `this.$route.meta.dataInMeta`.

# Deeper Vue.js Routing (nested children)

You can go deeper into child components (up to server limits).

Let’s make a child route for the child route:
```javascript
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    children: [
      {
        path: "/board/:id",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            component: {
              render: function(h) {
                return h("div", ["I'm Child with prop", this.propToChild]);
              },
              props: {
                propToChild: {
                  type: Number,
                  required: true,
                  default: null,
                },
              },
            },
          },
        ],
      },
    ],
  },
];
```

The render function is now written as a regular function since you need a component context:

```vue

<template>
  <div>
    Board with prop id: {{ id }}
    <router-view :prop-to-child="parseInt(id)" />
  </div>
</template>
<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
};
</script>
```
We pass the parameters to the child component of the child component through the `router-view` component like a normal component. It sounds complicated but intuitive. And so, we lower the props in the child — child of the child: `<router-view :prop-to-child="parseInt(id)" />`

# Explanation of Path

The view `path: "child"` means that we refer to the parent's path and continue its `path:{parent-route}/child`

Any other level of the route can be referenced from the child component:

```javascript
children: [
      {
        path: "/first-level",
        ....
      }
    ]
```

This entry processes a page with the address: **http://localhost:8080/first-level**.

# Wider Vue.js Routing (multiple router-views)

Can be used multiple `router-view` in 1 component. To do this, in the routes configuration, we write instead of component - components, which takes an object, where the key is the `name` attribute `router-view`. If you specify the key "default", then such a component will be displayed if it is `router-view` unnamed (without an attribute `name`).

`/src/main.js` :
```javascript
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    children: [
      {
        path: "/board/:id",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            components: {
              default: { render: (h) => h("div", ["I'm Default"]) },
              user: { render: (h) => h("div", ["I'm User"]) },
              guest: { render: (h) => h("div", ["I'm Guest"]) },
            },
          },
        ],
      },
    ],
  },
];
```

`/components/Board.vue` :
```vue
<template>
  <div>
    Board with prop id: {{ id }}
    <div>
      <label for="is-user">
        Is User?
        <input v-model="isUser" id="is-user" type="checkbox" />
      </label>
      <router-view :prop-to-child="parseInt(id)" />
      <router-view v-if="isUser" name="user" />
      <router-view v-else name="guest" />
    </div>
  </div>
</template>
<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
  data() {
    return {
      isUser: false,
    };
  },
};
</script>
```
Let’s go to the address: `http://localhost:8080/board/23/child` and see a small interactive with switching active `router-view`s.

# 404 error page
To create an error page, just put the following construction at the end of the list of routes:
```javascript

{
  path: "*",
  component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
```
Now, when following a nonexistent path (for example, ** http://localhost:8080/mistake **), an error component will be displayed.
It is better to write in this form:

```javascipt
{
  path: "/page-not-found",
  alias: '*',
  component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
```
Now we have an error page where we can redirect users with a clear conscience (what if one day we need to do this).

# Route protection
Route protection is performed using route metadata and a `beforeEach` router hook:

```javascript
import Vue from "vue";
import App from "@/App.vue";
import VueRouter from "vue-router";
import HelloWorld from "@/components/HelloWorld";
import Board from "@/components/Board";
Vue.use(VueRouter);
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    meta: {
      requiresAuth: true,
    },
    children: [
      {
        path: "/board/:id",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            components: {
              default: { render: (h) => h("div", ["I'm Default"]) },
              user: { render: (h) => h("div", ["I'm User"]) },
              guest: { render: (h) => h("div", ["I'm Guest"]) },
            },
          },
        ],
      },
    ],
  },
  {
    path: "/auth-required",
    component: { render: (h) => h("div", ["Auth required!"]) },
  },
  {
    path: "/*",
    component: { render: (h) => h("div", ["404! Page Not Found!"]) },
  },
];
const router = new VueRouter({
  routes,
  mode: "history",
});
const isAuthenticated = () => false;
router.beforeEach((to, from, next) => {
  if (to.matched.some((route) => route.meta?.requiresAuth)) {
    if (isAuthenticated()) {
      next();
    } else {
      next("/auth-required");
    }
  } else {
    next();
  }
});
Vue.config.productionTip = false;
new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");
```

Now, when trying to access a page that requires authorization, we will be redirected to the `/auth-required` page.
___

# Navigation between routes
## Software navigation

Programmatic navigation can be called from anywhere in your application like this:

`$router.push('/dash/23/child')`

If we want to pass parameters, we need to use a different approach based on the use of route names.
Let’s specify the name of the route `/board/:id` :

```javascript
...
   children: [
      {
        path: "/board/:id",
        name: 'board',
        component: Board,
        props: true,
        children: [
   ....
```
Now we can pass parameters:
`$router.push({ name: 'board', params: { id: 100500 }})`

We will get an error «Invalid prop: type check failed for prop «id». Expected String with value «100500», got Number with value 100500».

The reason is that `url-` it is always a data type String, and we passed it programmatically `id` with a type `Number`. The fix is ​​simple: we list the possible data types in the component.

`components/Board.vue` :

```vue
props: {
  id: {
    type: [String, Number],
    default: null,
  },
},
```

## RouterLink component
The component `routerLink` allows you to create links within the site, which are converted into "native" browser links (tag `<а>`):

`<router-link to='/dash/23/child'> Link </router-link>`

Classes can be automatically added to such links:
+ `router-link-exact-active` - exact match;
+ `router-link-active` - partial (the child component specified in the `to` route attribute is active).

In order not to display the active parent class, it is enough to write the attribute exact:

`<router-link to='/dash/23/child' exact> Link </router-link>`

We can override the element we create:

`<router-link tag="button" to='/dash'> Button </router-link>`

Unfortunately, in this case, the classes are not assigned.

We can also pass an object:

`<router-link :to="{ path: '/dash/23' "> Link </router-link>`

`<router-link :to="{ name: 'board', params: { id: 123 } }"> Link </router-link>`

# Best practics

We will devote this section to refactoring what we wrote above.

Create a folder structure for the router:
```bash
src/router/router.js
src/router/routes.js
```
Let’s transfer everything related to router settings to `router.js`:

```javascript
mport Vue from "vue";
import VueRouter from "vue-router";
import routes from "/routes";
Vue.use(VueRouter);
const router = new VueRouter({
  routes,
  mode: "history",
  base: process.env.BASE_URL,
});
const isAuthenticated = () => true;
router.beforeEach((to, from, next) => {
  if (to.matched.some((route) => route.meta?.requiresAuth)) {
    if (isAuthenticated()) {
      next();
    } else {
      next("/auth-required");
    }
  } else {
    next();
  }
});
export default router;
```

Let’s transfer `routes.js` everything related to route settings.
And immediately replace the imports with dynamic ones.
If you already have a lot of routes assigned, manual changes can be time-consuming. The regular will help:

`^import (\w+) from (".+")$`

replaced by

`const $1 = () => import(/* webpackChunkName: "$1" */ $2)`

Now in Chrome Dev Tools, in the Network tab, you will see when-which component is loaded from the network, and earlier all routes were loaded immediately in 1 mega-bundle.

`/src/router/routes.js` :

```javascript
const HelloWorld = () => import(/* webpackChunkName: "HelloWorld" */ "@/components/HelloWorld")
const Board = () => import(/* webpackChunkName: "Board" */ "@/components/Board")
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    meta: {
      requiresAuth: true,
    },
    children: [
      {
        path: "/board/:id",
        name: "board",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            components: {
              default: { render: (h) => h("div", ["I'm Default"]) },
              user: { render: (h) => h("div", ["I'm User"]) },
              guest: { render: (h) => h("div", ["I'm Guest"]) },
            },
          },
        ],
      },
    ],
  },
  {
    path: "/auth-required",
    component: { render: (h) => h("div", ["Auth required!"]) },
  },
  {
    path: "/*",
    component: { render: (h) => h("div", ["404! Page Not Found!"]) },
  },
];
export default routes;
```

# Advanced tricks in Vue.s Routing

By “advanced” is meant the “pleasantness” of using them. Such techniques include, for example, topics such as:
+ division of rights by access levels;
+ animation of transitions between pages;
l+ ading indication when switching between routes;
+ changing titles when switching between routes;
+ smooth scrolling across the page when moving back;
+ etc.
So, everything in order.

# Splitting rights by access levels

There is a situation when users have more than two states: not only authorization but others as well. For example, a paid subscription. From now on, we are thinking about an unlimited level of separation of rights. This is done in just a couple of dozen lines of code, but for brevity, convenience and not to reinvent the wheel, we will use a ready-made library. Let’s install it:

```bash
yarn add vue-router-middleware-plugin
```

Let’s create special middleware files for checking user rights:
`router/middleware/authMiddleware.js` :

```javascript
const isLoggedIn = () => !!window.localStorage.getItem("logged-in")
const authMiddleware = async ({ /* to, from to,*/ redirect }) => {
  if (!isLoggedIn()) {
    redirect({
      name: "login",
    });
  }
};
export default authMiddleware;
```
`router/middleware/guestMiddleware.js` :

```javascript

const isLoggedIn = () => !!window.localStorage.getItem("logged-in");
const guestMiddleware = async ({ /* to, from to,*/ redirect }) => {
  if (isLoggedIn()) {
    redirect({ name: "main" });
  }
};
export default guestMiddleware;
```
`router/middleware/subscribersMiddleware.js` :

```javascript
const isSubscribed = () => Promise.resolve(!!window.localStorage.getItem("has-license"))
const subscribersMiddleware = async ({ /* to, from, */ redirect }) => {
  if (!await isSubscribed()) {
    console.log("isn't subscribed, redirect to license")
    redirect({ name: 'license' })
  }
}
export default subscribersMiddleware
```

The last listing shows an example of an asynchronous check, which means that you can access the store’s actions and make requests to the server.
Now let’s put an authorization check on all routes, and then we’ll make exceptions for some routes:
`/src/router/router.js` :

```javascript
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
import MiddlewarePlugin from "vue-router-middleware-plugin";
import authMiddleware from "./middleware/authMiddleware";


Vue.use(VueRouter);
const router = new VueRouter({
  routes,
  mode: "history",
  base: process.env.BASE_URL,
});
Vue.use(MiddlewarePlugin, {
  router,
  middleware: [authMiddleware],
});
export default router;
```
Now let’s deal with specific routes.
Let’s work on the architecture of our application to make it more predictable. Let’s make a separate Auth.vue template and put it in pages, and the components that are used there, i.e. in the `/auth` section, put components in the appropriate section.

So a convenient structure is obtained:

```
pages
--Auth.vue
components
-- auth
---- Login.vue
---- Register.vue
---- Forgot.vue
```
Let’s create a helper function to generate such routes `genAuthRoutes`.

`/src/router/routes.js` :

```javascript
import guestMiddleware from "./middleware/guestMiddleware";
import authMiddleware from "./middleware/authMiddleware";
import subscribersMiddleware from "./middleware/subscribersMiddleware";
const MainBoard = () =>
  import(/* webpackChunkName: "MainBoard" */ "@/pages/MainBoard");
const BoardComponent = () =>
  import(
    /* webpackChunkName: "BoardComponent" */ "@/components/board/BoardComponent"
  );
const clearAndUpper = (text) => text.replace(/-/, "").toUpperCase();
const toPascalCase = (text) => text.replace(/(^\w|-\w)/g, clearAndUpper);
const genAuthRoutes = ({ parent, tabs = [] }) => ({
  path: `/${parent}`,
  name: parent,
  component: () => import(/* webpackChunkName: "auth" */ "@/pages/Auth"),
  redirect: { name: tabs[0] },
  children: tabs.map((tab) => {
    const tabPascalCase = toPascalCase(tab);
    return {
      path: tab,
      name: tab,
      component: () =>
        import(
          /* webpackChunkName: "[request]" */ `@/components/${parent}/${tabPascalCase}`
        ),
      meta: {
        middleware: {
          ignore: [authMiddleware],
          attach: [guestMiddleware],
        },
      },
    };
  }),
}); 
const routes = [
  genAuthRoutes({ parent: "auth", tabs: ["login", "register", "forgot"] }),
  {
    path: "/",
    name: "main",
    component: MainBoard,
    children: [
      {
        path: "/board",
        name: "board",
        component: {
          render: (h) => h("div", ["Board Page", h("router-view")]),
        },
        children: [
          {
            path: "/board/:id",
            name: "board-child",
            component: BoardComponent,
            props: true,
            children: [
              {
                path: "child",
                components: {
                  default: { render: (h) => h("div", ["I'm Default"]) },
                  user: { render: (h) => h("div", ["I'm User"]) },
                  guest: { render: (h) => h("div", ["I'm Guest"]) },
                },
                meta: {
                  middleware: {
                    attach: [subscribersMiddleware],
                  },
                },
              },
            ],
          },
        ],
      },
      {
        path: "/license",
        name: "license",
        component: {
          render: (h) => h("div", ["License Page"]),
        },
      },
    ],
  },
  {
    path: "/auth-required",
    name: "auth-required",
    component: { render: (h) => h("div", ["Auth required!"]) },
    meta: {
      middleware: {
        ignore: [authMiddleware],
      },
    },
  },
  {
    path: "/*",
    component: { render: (h) => h("div", ["404! Page Not Found!"]) },
    meta: {
      middleware: {
        ignore: [authMiddleware],
      },
    },
  },
];
export default routes;
```

We remove the global authorization check in the property ignoreand add another check in the attachobject property `meta.middleware`:

```middleware: {
    ignore: [authMiddleware],
    attach: [guestMiddleware], 
 }
```
Let’s create the components:

+ /src/components/auth/Login.vue;
+ /src/components/auth/Register.vue;
+ /src/components/auth/Forgot.vue,

with a typical template:
```html
<template>
  <div>
    Forgot Page
  </div>
</template>
```

We’ll also refactor the page `Board`, let's call it `MainBoard`

`/src/pages/MainBoard.vue` :

```vue
<template>
  <div>
    <h1>Main Board Page</h1>
    <router-view />
  </div>
</template>
```
Accordingly, we add components to the appropriate category in components:

`/src/components/board/BoardComponent.vue`:

```vue
<template>
  <div>
    Board with prop id: {{ id }}
    <div>
      <label for="is-user">
        Is User?
        <input v-model="isUser" id="is-user" type="checkbox" />
      </label>
      <router-view :prop-to-child="parseInt(id)" />
      <router-view v-if="isUser" name="user" />
      <router-view v-else name="guest" />
    </div>
  </div>
</template>
<script>
export default {
  props: {
    id: {
      type: [String, Number],
      default: null,
    },
  },
  data() {
    return {
      isUser: false,
    };
  },
};
</script>
```

It remains to refactor the main component — `/src/App.vue`:

```vue
<template>
  <div id="app">
    <div class="links">
      <router-link :to="{ name: 'register' }">Register</router-link>
      <router-link :to="{ name: 'login' }">Login</router-link>
      <router-link :to="{ name: 'forgot' }">Forgot</router-link>
      <template v-if="loggedIn">
        <router-link :to="{ name: 'license' }">License</router-link>
        <router-link :to="{ name: 'board' }">Board</router-link>
        <router-link :to="{ name: 'board-child', params: { id: 33 } }"
          >Board:33</router-link
        >
        <router-link :to="{ path: '/board/33/child' }"
          >Board:33/child</router-link
        >
        <router-link :to="{ path: '/404' }">404</router-link>
      </template>
      <label for="logged-in"
        >Logged In
        <input type="checkbox" id="logged-in" v-model="loggedIn" />
      </label>
      <label for="has-license"
        >Has License
        <input type="checkbox" id="has-license" v-model="hasLicense" />
      </label>
    </div>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      loggedIn: !!window.localStorage.getItem("logged-in"),
      hasLicense: !!window.localStorage.getItem("has-license"),
    };
  },
  watch: {
    loggedIn(e) {
      window.localStorage.setItem("logged-in", e ? true : "");
    },
    hasLicense(e) {
      window.localStorage.setItem("has-license", e ? true : "");
    },
  },
};
</script>
<style scoped>
.links > * {
  margin: 1em;
}
</style>
```

Now, uncheck **“Logged In”** and try to follow the route **http: //localhost:8080/board**. We will be immediately redirected to the **“auth-required”** page.


Check **“Logged In”**, uncheck **“Has License”** and navigate to **http://localhost: 8080/board/33/child**. We will be taken to the license page, however, if you uncheck “Logged In” and refresh the page, then we will go back to the **“auth-required”** page.


Now let’s check if it is possible to enter the authorization page when the user has already been authorized. Check the box **“Logged In”** and go to **http://localhost:8080/auth/register**. We will be redirected to the main page.

# Read More
If you found this article helpful, click the💚 or 👏 button below or share the article on Facebook so your friends can benefit from it too.