DEV Community

Cover image for Routing in Vue.js — Starting Guide
Mikhail Raevskiy
Mikhail Raevskiy

Posted on • Originally published at Medium

Routing in Vue.js — Starting Guide

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:

<script src="https://unpkg.com/vue-router"></script>
Enter fullscreen mode Exit fullscreen mode

But we’ll start right away with the “correct” option — Vue Cli:

yarn global add @vue/cli
# OR
npm i -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

Let’s create a project using the VUE CLI with a basic template — Default ([Vue 2] babel, eslint):

vue create vue-router-test-app
Enter fullscreen mode Exit fullscreen mode

Minimal configuration

Add a router:

yarn add vue-router
# OR
npm i --save vue-router
Enter fullscreen mode Exit fullscreen mode

Let’s add the minimal router configuration to /src/main.js:

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");
Enter fullscreen mode Exit fullscreen mode

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:

<template>
  <div id="app">
    <router-view /> 
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

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):

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page"]),
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

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:


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,
      }
    ]
  },
];
Enter fullscreen mode Exit fullscreen mode

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

<template>
  <div>
    Board Page
    <router-view />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

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

<template>
  <div>Board with prop id: {{ id }}</div>
</template>
<script>
export default {
  computed: {
    id() {
      return this.$route.params.id;
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

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:

children: [
  {
    path: '/board/:id',
    component: Board,
    props: true,
  }
]
Enter fullscreen mode Exit fullscreen mode

And in the /src/components/Board.vue component, accept idas an input parameter of the component:

<template>
  <div>Board with prop id: {{ id }}</div>
</template>
<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Route meta


const routes = [
    {
      path: "",
      component: HelloWorld,
      meta: {
        dataInMeta: "test",
      },
    },
     ....
   ]
Enter fullscreen mode Exit fullscreen mode

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:

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,
                },
              },
            },
          },
        ],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

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


<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>
Enter fullscreen mode Exit fullscreen mode

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:

children: [
      {
        path: "/first-level",
        ....
      }
    ]
Enter fullscreen mode Exit fullscreen mode

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 :

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"]) },
            },
          },
        ],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

/components/Board.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>
Enter fullscreen mode Exit fullscreen mode

Let’s go to the address: http://localhost:8080/board/23/child and see a small interactive with switching active router-views.

404 error page

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


{
  path: "*",
  component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
Enter fullscreen mode Exit fullscreen mode

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:

{
  path: "/page-not-found",
  alias: '*',
  component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
Enter fullscreen mode Exit fullscreen mode

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:

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");
Enter fullscreen mode Exit fullscreen mode

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 :

...
   children: [
      {
        path: "/board/:id",
        name: 'board',
        component: Board,
        props: true,
        children: [
   ....
Enter fullscreen mode Exit fullscreen mode

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 :

props: {
  id: {
    type: [String, Number],
    default: null,
  },
},
Enter fullscreen mode Exit fullscreen mode

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:

src/router/router.js
src/router/routes.js
Enter fullscreen mode Exit fullscreen mode

Let’s transfer everything related to router settings to router.js:

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;
Enter fullscreen mode Exit fullscreen mode

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 :

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;
Enter fullscreen mode Exit fullscreen mode

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:

yarn add vue-router-middleware-plugin
Enter fullscreen mode Exit fullscreen mode

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

const isLoggedIn = () => !!window.localStorage.getItem("logged-in")
const authMiddleware = async ({ /* to, from to,*/ redirect }) => {
  if (!isLoggedIn()) {
    redirect({
      name: "login",
    });
  }
};
export default authMiddleware;
Enter fullscreen mode Exit fullscreen mode

router/middleware/guestMiddleware.js :


const isLoggedIn = () => !!window.localStorage.getItem("logged-in");
const guestMiddleware = async ({ /* to, from to,*/ redirect }) => {
  if (isLoggedIn()) {
    redirect({ name: "main" });
  }
};
export default guestMiddleware;
Enter fullscreen mode Exit fullscreen mode

router/middleware/subscribersMiddleware.js :

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
Enter fullscreen mode Exit fullscreen mode

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 :

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let’s create a helper function to generate such routes genAuthRoutes.

/src/router/routes.js :

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

/src/pages/MainBoard.vue :

<template>
  <div>
    <h1>Main Board Page</h1>
    <router-view />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Accordingly, we add components to the appropriate category in components:

/src/components/board/BoardComponent.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>
Enter fullscreen mode Exit fullscreen mode

It remains to refactor the main component — /src/App.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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
donpuerto_ profile image
donpuerto

Wow thanks