Guards are functions that control the navigation flow. They can allow, cancel, or redirect navigation.
When navigating from /a to /b:
router:beforeEach event (can block)router.onBeforeEach()beforeLeave guard on /abeforeEnter guard on /b| Return Value | Type | Effect |
|---|---|---|
true |
boolean |
Allow navigation |
undefined |
void |
Allow navigation (implicit) |
false |
boolean |
Cancel navigation |
"/path" |
string |
Redirect to path |
{ path: "/path", ... } |
object |
Redirect with options |
app.use(Router, {
mount: "#app",
routes: [...],
onBeforeEach: (to, from) => {
console.log(`Navigating: ${from?.path || 'initial'} → ${to.path}`);
// Allow navigation
return true;
}
});
// Guard 1: Logging
const unsubLog = router.onBeforeEach((to, from) => {
console.log(`Navigation: ${from?.path} → ${to.path}`);
// No return = allow
});
// Guard 2: Authentication
const unsubAuth = router.onBeforeEach((to, from) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
return "/login"; // Redirect to login
}
});
// Guard 3: Authorization
const unsubRoles = router.onBeforeEach((to, from) => {
if (to.meta.roles && !hasAnyRole(to.meta.roles)) {
return { path: "/error", query: { code: "403" } };
}
});
// Later: Remove specific guards
unsubLog(); // Remove logging guard
unsubAuth(); // Remove auth guard
{
path: "/settings",
component: SettingsPage,
meta: { requiresAuth: true },
// Runs before entering this route
beforeEnter: async (to, from) => {
// Async validation
const canAccess = await checkPermissions();
if (!canAccess) {
return "/unauthorized";
}
// Allow navigation (implicit return)
},
// Runs before leaving this route
beforeLeave: (to, from) => {
if (hasUnsavedChanges()) {
// Browser confirm dialog
const confirmed = confirm("You have unsaved changes. Leave anyway?");
return confirmed; // true = allow, false = cancel
}
}
}
Guards can be async functions for API calls, permission checks, etc.
router.onBeforeEach(async (to, from) => {
if (to.meta.requiresAuth) {
try {
// Validate session with server
const response = await fetch("/api/auth/validate");
const { valid } = await response.json();
if (!valid) {
return {
path: "/login",
query: { redirect: to.path } // Remember where they wanted to go
};
}
} catch (error) {
console.error("Auth check failed:", error);
return "/error";
}
}
});
router.onBeforeEach((to, from) => {
// `to` - Target route location
console.log(to.path); // "/users/123"
console.log(to.params); // { id: "123" }
console.log(to.query); // { tab: "settings" }
console.log(to.meta); // { requiresAuth: true }
console.log(to.fullUrl); // "/users/123?tab=settings"
console.log(to.matched); // Route definition object
// `from` - Source route location (null on initial navigation)
if (from) {
console.log(from.path); // "/dashboard"
}
});
Hooks are functions for side effects. Unlike guards, they cannot block navigation.
| Hook | When Called | Use Cases |
|---|---|---|
onAfterEnter |
After new component mounted | Update title, focus element |
onAfterLeave |
After old component unmounted | Cleanup resources |
onAfterEach |
After navigation completes | Analytics, logging |
onError |
On navigation error | Error reporting |
// Update document title after entering a route
const unsubTitle = router.onAfterEnter((to, from) => {
document.title = to.meta.title || "My App";
});
// Clean up resources after leaving a route
const unsubCleanup = router.onAfterLeave((to, from) => {
// Close any open modals
closeAllModals();
// Abort pending requests for the old route
abortPendingRequests(from?.path);
});
// Track page views
const unsubAnalytics = router.onAfterEach((to, from) => {
analytics.pageView({
path: to.path,
title: to.meta.title,
referrer: from?.path
});
});
// Report navigation errors
const unsubErrors = router.onError((error, to, from) => {
errorReporter.capture(error, {
context: "navigation",
to: to?.path,
from: from?.path
});
});
All hook methods return an unsubscribe function:
// Store unsubscribe functions
const cleanupFns = [];
cleanupFns.push(router.onAfterEach((to) => { /* ... */ }));
cleanupFns.push(router.onError((err) => { /* ... */ }));
// Later, clean up all hooks
function cleanup() {
cleanupFns.forEach(fn => fn());
cleanupFns.length = 0;
}
router.onBeforeEach((to, from) => {
if (to.meta?.requiresAuth && !isAuthenticated()) {
return { path: "/login", query: { redirect: to.path } };
}
return true;
});
router.onBeforeEach((to, from) => {
if (to.meta?.roles) {
const userRoles = getCurrentUserRoles();
const hasAccess = to.meta.roles.some(role => userRoles.includes(role));
if (!hasAccess) return "/unauthorized";
}
});
router.onBeforeEach((to, from) => {
if (from?.meta?.hasUnsavedChanges && hasUnsavedChanges()) {
const confirmed = confirm("You have unsaved changes. Leave anyway?");
if (!confirmed) return false;
}
});
router.onBeforeEach(async (to, from) => {
if (to.meta?.checkPermission) {
const allowed = await checkPermissionAPI(to.path);
if (!allowed) return "/forbidden";
}
});
| ← Back to Navigation | Next: Lazy Loading → |