Vue.js 3 Route Guards Tutorial

In this Vue tutorial we learn how to execute custom logic before or after a user goes through a route with route guards.

We cover the global and local route and in-component guards as well as the next method argument.

Lesson Project

If you want to follow along with the examples in this lesson, you will need an app generated by the Vue CLI with the Router package installed , as well as the following extra components.

  • src/router/index.js
  • src/views/Home.vue
  • src/views/Users.vue

The project should look similar to the following.

Example: project
project_folder/
├── src/
|   ├── components/
|   ├── router/
|   |   └── index.js
|   ├── views/
|   |   ├── Home.vue
|   |   └── Users.vue
|   └── App.vue

The /src/router/index.js file is where our routes are defined.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

export default router

We link to the routes in the root App component and show the router-view below that.

Example: src/App.vue
<template>
  <div>
    <router-link :to="{ name: 'Home' }">Home</router-link> |
    <router-link :to="{ name: 'Users' }">Users</router-link>
  </div>

  <router-view/>
</template>

The Home and Users views each have a heading that allows us to easily identify them.

Example: src/views/Home.vue
<template>
  <h2>Home page</h2>
</template>
Example: src/views/Users.vue
<template>
  <h2>Users page</h2>
</template>

What is a Route Guard?

A route guard is a function that can execute custom logic before or after a user goes through a route.

For example, we can restrict access to a member area unless a user is authenticated through a login system. Or we can warn a user of any unsaved changes in a form before they navigate away.

The Vue Router package gives us access to 3 hooks. The function for each hook depends on where it’s used.

  • Global. The hooks will affect all routes.
  • Local, in the route definition. The hook will affect only the route it’s attached to.
  • Local, in a component. The hook will affect only the route for that component.

Each guard takes a callback (anonymous or arrow) function as argument that automatically receives 2 parameters.

  • to is route we’re navigating to.
  • from is the route we’re coming from.

A guard can return a false value if we need to prevent navigation through a route. Or, it can return a route path that we want to redirect to.

Example:
// arrow function as param
hook((to, from) => {

  // optionally return false
  // to cancel the navigation
  return false
})

// or anonymous function
hook(function(to, from) {

  // optionally return
  // path to redirect to
  return '/login'
})

The callback functions can also be async.

Example: async
hook(async (to, from) => {

  const auth = await auth()
})

Global route guards

The global route guard hooks are as follows.

  • beforeEach . Before navigating to the route.
  • beforeResolve . After in-component guards have been executed but before the navigation happens.
  • afterEach . After a view has been loaded.

We call these hooks on the router config object before we pass it to our app with the use method.

Example: global route guard
// create & config router
const router = createRouter({})

// route guards
router.beforeEach((to, from) => {})

// use router
app.use(router)

// mount app
app.mount('#app')

If we configured the router in a separate file, we call them before the export.

Example: separate file
// create & config router
const router = createRouter({})

// route guards
router.beforeEach((to, from) => {})

// export config
export default router

The beforeEach route guard

The beforeEach route guard will run before any route in our router is loaded.

A possible use case is restricting access from a member area until a user has logged in.

As an example, let’s restrict access to everything in our app except the Home page.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

router.beforeEach(() => {
  // redirect to home path
  return '/'
})

export default router

When we run the example in the browser and try to navigate to the Users page, it won’t let us. So our route guard works as expected.

But there’s a problem that’s not immediately obvious. If we open up the console, we’ll see that Vue stopped the app from executing.

Output:
[Vue Router warn]:
Detected an infinite redirection in a navigation guard
when going from "/" to "/". Aborting to avoid a Stack
Overflow. This will break in production if not fixed.

Remember that beforeEach is global. It will redirect all routes, including the one we return, causing an infinite loop.

We want to redirect to the Home page only if we’re not already on it. So we can use the to parameter to access the route name and evaluate that we’re not on the Home page.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

router.beforeEach((to) => {
  // only redirect to 'Home'
  // if we're not already on it
  if (to.name !== 'Home') {
    return '/'
  }
})

export default router

If we run the example in the browser, the route guard still works and the warning in the console is gone.

The beforeResolve route guard

The beforeResolve route guard will run after all in-component guards have been executed, but before the navigation happens. For example, after a user has been authenticated, but before the member area page is loaded.

A use case would be to fetch a user’s data from an API because we already know the user is authenticated.

To keep our example simple, we’ll log a message to the console instead of creating a user auth system.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

router.beforeResolve((to, from) => {
  console.log('User Authenticated')
  console.log('Coming from:', from.path)
  console.log('Going to:', to.path)
})

export default router

If we run the example in the browser and open the console, we will see the messages when we navigate through the pages.

The afterEach route guard

The afterEach route guard will run after the navigation has happened. This guard cannot block access like the beforeEach guard.

A use case would be to send analytics data to your storage layer.

For our example, we’ll just log messages to the console.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

router.beforeEach(() => {
  console.log('Before route loaded')
})

router.beforeResolve(() => {
  console.log('After route loaded, before navigation')
})

router.afterEach(() => {
  console.log('After navigation')
})

export default router

note afterEach is executed each time the view is loaded, even if the user is currently on that page.

If we navigate to the Users view and click on the link a few times, we’ll see that the console log is executed multiple times.

Local beforeEnter route definition guard

There is only one guard we can define inside the route array, beforeEnter .

It works the same as beforeEach , except it’s only executed for the route we define it in. The to and from parameters are also specified in its parameter list, instead of in a callback function.

Syntax: beforeEnter
{
  path: '/url-path',
  name: 'Route_name',
  component: () => import('path/to/component'),

  // beforeEnter takes direct
  // arguments instead of a
  // function
  beforeEnter(to, from) {

    // block navigation
    return false
  }
}

For our example, we’ll block access to the Users page and show an Alert message.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users'),
      beforeEnter() {
        alert('You are not authorized to view this page')
        // block navigation
        return false
      }
    }
  ]
})

export default router

Like with all the guard hooks, we can redirect to another page by returning the path.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home.vue')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users.vue'),
      beforeEnter(to) {
        alert('Please login first')
        // redirect back home
        if (to.name !== 'Home') {
          return '/'
        }
      },
      children: [
        {
          path: '/user/:id',
          name: 'UserSingle',
          component: () => import('../views/UserSingle.vue'),
          props: true
        }
      ]
    }
  ]
})

export default router

This time, we’re redirected back to the Home page after the alert.

Local component guards

The local in-component route guards are as follows.

  • beforeRouteEnter . Before navigating to a route.
  • beforeRouteUpdate . Before a reused route has been changed.
  • beforeRouteLeave . Before this route has been navigated away from.

These hooks are called from within the component that we want to guard as an option function. As with beforeEach , the option takes the to and from parameters directly, instead of in a callback function.

Example: component level
<script>
export default {
  beforeRouteEnter(to, from) {

    return 'false'
    // or
    return 'redirect-path'
  }
}
</script>

All in-component guards are executed before the global beforeResolve guard.

The beforeRouteEnter in-component guard

The beforeRouteEnter guard executes before the component is loaded.

As an example, let’s add the guard to our Users view and block access to it.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>
</template>

<script>
export default {
  beforeRouteEnter(to) {
    alert('Please login first')
    // redirect back home
    if (to.name !== 'Home') {
      return '/'
    }
  }
}
</script>

If you have the beforeEnter guard on the route definition from the previous example, it will override the component-level guards so it should be removed to make the example above work.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

export default router

The beforeRouteUpdate in-component guard

The beforeRouteUpdate guard is executed when the route that renders a component has changed.

As an example, let’s add a new page in /src/views/UserSingle.vue .

Example: project
project_folder/
├── src/
|   ├── components/
|   ├── router/
|   |   └── index.js
|   ├── views/
|   |   ├── Home.vue
|   |   ├── Users.vue
|   |   └── UserSingle.vue
|   └── App.vue

It will take a route parameter as a prop and display the user’s id.

Example: src/views/UserSingle.vue
<template>
  <h2>Single User page</h2>
  <p>User ID: {{ id }}</p>
</template>

<script>
export default {
  props: ['id']
}
</script>

In the route definitions, we can add it as a child to the Users route.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users'),
      children: [
        {
          path: '/user/:id',
          name: 'UserSingle',
          component: () => import('../views/UserSingle'),
          props: true
        }
      ]
    }
  ]
})

export default router

In the Users view, we’ll define some users and link to each with their id as the route parameter. We’ll also add the guard and have it log a message to the console.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>

  <p v-for="user in users" :key="user.id">
	<router-link
	  :to="{
		name: 'UserSingle',
		params: { id: user.id }
	  }">
	  ({{ user.id }}) {{user.name}}
	</router-link>
  </p><hr>

  <router-view/>
</template>

<script>
export default {
  beforeRouteUpdate() {
	console.log('Route updated')
  },
  data() {
	return {
	  users: [
		{ id: 1, name: 'John' },
		{ id: 2, name: 'Jane' },
		{ id: 3, name: 'Jack' },
		{ id: 4, name: 'Jill' }
	  ]
	}
  }
}
</script>

What happens is that when we navigate between users/1 and users/2 , the same users/ route is used so beforeRouteUpdate is executed.

Tip: Because the component is already mounted at this point, we can use this.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>

  <p v-for="user in users" :key="user.id">
    <router-link
      :to="{
        name: 'UserSingle',
        params: { id: user.id }
      }">
      ({{ user.id }}) {{ user.name }}
    </router-link>
  </p><hr>

  <router-view/>
</template>

<script>
export default {
  beforeRouteUpdate() {
    // has access to 'this'
    this.users.forEach((user) => {
      console.log(user.id, user.name)
    })
  },
  data() {
    return {
      users: [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
        { id: 3, name: 'Jack' },
        { id: 4, name: 'Jill' }
      ]
    }
  }
}
</script>

If we run the example, navigate to the Users page and click on a user’s name, it will show a console log of all the users.

The beforeRouteLeave in-component guard

The beforeRouteLeave guard is executed just before the user navigates away from the route.

A common use case for this guard is to warn a user that they have unsaved changes in a form.

To keep the example simple, we’ll just add a browser confirmation message that asks the user if they want to leave.

Example: src/views/Home.vue
<template>
  <h2>Home page</h2>
</template>

<script>
export default {
  beforeRouteLeave() {

    const reply = window.confirm('You have unsaved changes! Do you want to leave?')

    if (!reply) {
      // stay on the page if
      // user clicks 'Cancel'
      return false
    }
  }
}
</script>

If we run the example and try to navigate to the Users page, the confirmation message will show.

tip Like the beforeRouteUpdate guard, we also have access to this in beforeRouteLeave .

The next guard method argument

In previous versions of the Vue Router, we could use the next optional third argument instead of returning from the guard.

Some confusion in it’s usage lead to mistakes in people’s code, so it went through an RFC to be removed. However, it’s still supported.

The next argument is a function and works the same as returning from the guard. If we want to confirm or deny a route, we pass true or false to it as an argument.

Example: next
beforeRouteLeave(to, from, next) {

  const reply = window.confirm('You have unsaved changes! Do you want to leave?')

  if (!reply) {
    // deny navigation
    next(false)
  } else {
    // proceed with navigation
    next(true)
  }
}

If we want to redirect to a different route, we add the path as an argument.

Example: next redirect
beforeRouteEnter(to, from, next) {
  // redirect to Users page
  next('/user')
}

note Next should only be used once, unless its usage doesn’t overlap.

Example: next overlap
beforeRouteLeave(to, from, next) {

  const reply = window.confirm('You have unsaved changes! Do you want to leave?')

  if (!reply) {
    next(false)
  }

  // will overlap if user clicks 'Cancel'
  // causing it to be called twice
  next(true)
}

It’s better to place the second next call in an else block.

Example: no overlap
beforeRouteLeave(to, from, next) {

  const reply = window.confirm('You have unsaved changes! Do you want to leave?')

  if (!reply) {
    next(false)
  } else {
    next(true)
  }
}

Further Reading

For more information on the topics covered in this lesson, please see the relevant section below.