Vue Router Navigation Guards: Implementation Guide and Best Practices

Overview

Vue Router provides navigation guards that enable developers to execute custom logic during route transitions. These hooks activate at critical moments in the navigation process, making them essential for implementing features like access control, data preloading, and route validation.

Navigation guards in Vue Router essentially serve as interceptors for monitoring route entry, updates, and exits.

Guard Parameters

Each guard receives three parameters:

  • to: The target route object being navigated to
  • from: The current route being navigated away from
  • next: A required function to resolve the guard and continue navigation

About the next() Function

The next() method is critical - navigation will not proceed unless it is called. Here are the available options:

  1. next(): Proceeds to the next guard in the pipeline. If all guards pass, navigation becomes confirmed.
  2. next(false): Cancels the current navigation. If the URL changed, it reverts to the from route.
  3. next('/path') or next({ path: '/path' }): Redirects to a different location, aborting current navigation.
  4. next(error): (2.4.0+) If an Error isntance is passed, navigation terminates and the error routes to router.onError().

Types of Navigation Guards

Vue Router supports three categories of navigation guards:

  • Global Guards: beforeEach, beforeResolve, afterEach
  • Per-Route Guards: beforeEnter
  • Component Guards: beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave

1. Global Guards

Global guards apply to the entire router instance. There are three main global guards:

beforeEach - Global Before Guard

Executes before every route navigation. Common use cases include authentication verification and redirecting unauthenticated users.

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // route definitions
  ]
});

router.beforeEach((to, from, next) => {
  // Check authentication status
  const isAuthenticated = localStorage.getItem('authToken');
  
  if (to.meta.requiresAuth && !isAuthenticated) {
    next({ name: 'Login', query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

beforeResolve - Global Resolve Guard

Introduced in version 2.5.0+, this guard executes after beforeRouteEnter but before navigation is confirmed. Ideal for prefetching data required by the target component.

router.beforeResolve(async (to, from, next) => {
  // Preload critical data before navigation completes
  const requiredData = await fetchComponentData(to.params.id);
  
  if (requiredData) {
    to.meta.preloadedData = requiredData;
    next();
  } else {
    next({ name: 'Error', params: { message: 'Data unavailable' } });
  }
});

afterEach - Global After Guard

Executes after navigation completes, regardless of success or failure. Suitable for cleanup tasks, analytics tracking, or scroll position management.

router.afterEach((to, from) => {
  // Reset scroll position on route change
  window.scrollTo(0, 0);
  
  // Update page title dynamically
  document.title = to.meta.pageTitle || 'Default Title';
});

2. Per-Route Guards

Route-specific guards can be defined directly within route configurations using beforeEnter. These guards only trigger when accessing that particular route.

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/admin',
      component: AdminDashboard,
      beforeEnter: (to, from, next) => {
        // Verify admin privileges
        const userRole = getUserRole();
        
        if (userRole === 'admin') {
          next();
        } else {
          next({ name: 'Forbidden' });
        }
      }
    }
  ]
});

3. Component Guards

Component-specific guards are defined within Vue components and provide fine-grained control over component-level navigation logic.

beforeRouteEnter

Executes before the component is mounted. At this point, the component instance is not yet created, so this is unavailable. Access the component instance through the next callback.

export default {
  data() {
    return {
      userProfile: null
    };
  },
  beforeRouteEnter(to, from, next) {
    // Component instance not available here
    fetchUserData(to.params.userId)
      .then(data => {
        next(vm => {
          // Access component instance via vm
          vm.userProfile = data;
        });
      })
      .catch(error => {
        next({ name: 'Error' });
      });
  }
};

beforeRouteUpdate

Executes when a route with dynamic parameters changes but the same component is reused. For example, navigating from /user/1 to /user/2 with a User component.

export default {
  data() {
    return {
      currentUser: null
    };
  },
  beforeRouteUpdate(to, from, next) {
    // Component instance available via 'this'
    this.fetchUserData(to.params.userId)
      .then(data => {
        this.currentUser = data;
        next();
      });
  }
};

beforeRouteLeave

Executes when navigating away from the current route. Commonly used for preventing accidental navigation with unsaved changes.

export default {
  data() {
    return {
      hasUnsavedChanges: false
    };
  },
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      const confirmLeave = window.confirm('You have unsaved changes. Leave anyway?');
      if (confirmLeave) {
        next();
      } else {
        next(false);
      }
    } else {
      next();
    }
  }
};

Vue 3 Composition API Usage

With Vue Router 4 and Composition API, use composable functions for navigation guards:

import { 
  onBeforeRouteEnter, 
  onBeforeRouteUpdate, 
  onBeforeRouteLeave 
} from 'vue-router';

export default {
  setup() {
    onBeforeRouteEnter((to, from, next) => {
      next(vm => {
        // Access component instance
      });
    });

    onBeforeRouteUpdate((to, from, next) => {
      // Handle parameter changes
      next();
    });

    onBeforeRouteLeave((to, from, next) => {
      // Cleanup before leaving
      next();
    });
    
    return {};
  }
};

Version Comparison

Vue Router 3 (Vue 2) Vue Router 4 (Vue 3)
beforeRouteEnter onBeforeRouteEnter
beforeRouteUpdate onBeforeRouteUpdate
beforeRouteLeave onBeforeRouteLeave

Guard Execution Order

Understanding the execution sequence is critical for proper guard implementation. The complete order is:

  1. Global beforeEach: Executes first on every navigation
  2. Per-route beforeEnter: Executes for the target route if defined
  3. Global beforeResolve: Executes after all beforeRouteEnter guards
  4. Component beforeRouteEnter: Executes before component instantiation
  5. Global afterEach: Executes after navigation confirms
  6. Component beforeRouteUpdate: Executes when route changes but component reuses
  7. Component beforeRouteLeave: Executes when navigating away

Route Authentication Implementation

Route authentication is a critical feature for protecting sensitive routes and controlling user access.

Step 1: Define Route Metadata

Add metadata to routes to indicate authentication requirements:

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: LoginPage,
    meta: {
      requiresAuth: false
    }
  },
  {
    path: '/settings',
    name: 'Settings',
    component: SettingsPage,
    meta: {
      requiresAuth: true,
      allowedRoles: ['user', 'admin']
    }
  }
];

Step 2: Implement Authentication Guard

Use global beforeEach to verify authentication before navigation:

router.beforeEach((to, from, next) => {
  const requiresAuth = to.meta.requiresAuth;
  const allowedRoles = to.meta.allowedRoles || [];
  
  if (requiresAuth) {
    const storedUser = JSON.parse(localStorage.getItem('userData'));
    
    if (!storedUser || !storedUser.token) {
      next({ 
        name: 'Login', 
        query: { redirect: to.fullPath } 
      });
      return;
    }
    
    // Validate token expiration
    const currentTime = Date.now();
    if (storedUser.expiresAt < currentTime) {
      localStorage.removeItem('userData');
      next({ 
        name: 'Login', 
        query: { redirect: to.fullPath } 
      });
      return;
    }
    
    // Check role-based access
    if (allowedRoles.length > 0 && !allowedRoles.includes(storedUser.role)) {
      next({ name: 'Forbidden' });
      return;
    }
  }
  
  next();
});

Complete Router Configuration

import { createRouter, createWebHistory } from 'vue-router';

let savedScrollPosition = 0;

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/dashboard',
      name: 'Dashboard',
      component: Dashboard,
      meta: {
        requiresAuth: true,
        pageTitle: 'Dashboard'
      }
    },
    {
      path: '/profile',
      name: 'Profile',
      component: Profile,
      meta: {
        requiresAuth: true,
        pageTitle: 'User Profile'
      }
    }
  ]
});

// Authentication guard
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) {
    const userData = JSON.parse(localStorage.getItem('userData'));
    
    if (!userData?.token) {
      next({ name: 'Login', query: { redirect: to.fullPath } });
      return;
    }
    
    if (userData.expiresAt < Date.now()) {
      localStorage.removeItem('userData');
      next({ name: 'Login', query: { redirect: to.fullPath } });
      return;
    }
  }
  
  // Save scroll position before navigating away
  if (from.path !== '/') {
    savedScrollPosition = window.scrollY;
  }
  
  next();
});

// Navigation complete handler
router.afterEach((to) => {
  if (to.path !== '/') {
    window.scrollTo(0, 0);
  } else {
    // Restore scroll position on back navigation
    requestAnimationFrame(() => {
      window.scrollTo(0, savedScrollPosition);
    });
  }
});

export default router;

Error Handling

Vue Router 4 (Vue 3)

Use router.onError to register error callbacks:

import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // route configurations
  ]
});

router.onError((error) => {
  console.error('Navigation error:', error);
  
  // Display error page or notification
  showErrorNotification('Navigation failed. Please try again.');
});

export default router;

Vue Router 3 (Vue 2)

In Vue Router 3, detect errors through the afterEach guard:

router.afterEach((to, from, failure) => {
  if (failure) {
    console.error('Navigation failure:', failure);
    
    // Handle different error types
    if (failure.type === 2) { // Redirect abort
      // Handle redirect cancellation
    } else if (failure.type === 4) { // Navigation error
      // Handle navigation error
    }
  }
});

The failure parameter is available in Vue Router 3.1.0+ and provides details about navigation failures.

Additional Components

router-link

Key properties of the router-link component:

  • Uses History API mode - base path handling is automatic
  • Intercepts click events to prevent full page reloads
  • Renders as an a tag by default; use tag prop for other elements
  • Supports name binding and query parameters
<router-link 
  :to="{ name: 'UserList', query: { status: 'active' } }" 
  tag="li"
>
  Active Users
</router-link>

router-view

The router-view component renders matched components based on the route:

  • Supports nested routes through children configuration
  • Works with named views using the name prop
  • Combines with transition and keep-alive for animation and caching
<transition name="slide-fade" mode="out-in">
  <keep-alive>
    <router-view></router-view>
  </keep-alive>
</transition>

The keep-alive component caches mounted components, preserving they state when navigating between routes.

Tags: vue-router vuejs navigation-guards javascript frontend

Posted on Thu, 04 Jun 2026 17:12:41 +0000 by firedrop