Skip to content

NavigationGuard ​

Navigation guard function type, used to perform validation, redirection, analytics, and other logic before/after navigation occurs.

Type Definition ​

ts
type NavigationGuardReturn = void | undefined | boolean | RouteLocationRaw | NavigationRedirect | Error | null

interface NavigationRedirect {
  location: RouteLocationRaw
  mode?: 'push' | 'replace' | 'relaunch'
}

type NavigationGuard = (
  to: RouteLocation,
  from: RouteLocation,
) => NavigationGuardReturn | Promise<NavigationGuardReturn>

Parameters ​

ParameterTypeDescription
toRouteLocationThe target route being navigated to
fromRouteLocationThe current route being navigated away from

Return Value ​

ts
// Return a value directly, the router handles it automatically
router.beforeEach(async (to, from) => {
  if (isLoggedIn()) return true
  return { name: 'login' }
})

Guard Type Classification ​

Global Before Guard ​

ts
const removeGuard = router.beforeEach((to, from) => {
  // Permission validation, login check, analytics, etc.
  if (to.meta.requireAuth && !isLoggedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
  return true
})

// Remove guard
removeGuard()

Global Resolve Guard ​

Executes after beforeEach and beforeEnter, commonly used to wait for async data loading to complete:

ts
router.beforeResolve(async (to) => {
  if (to.meta.preload) {
    await store.preloadData(to.meta.preload)
  }
  return true
})

Global After Hook ​

Executes after navigation completes, cannot change navigation flow:

ts
router.afterEach((to, from) => {
  // Set title
  if (to.meta.title) {
    uni.setNavigationBarTitle({ title: to.meta.title as string })
  }
  // Page analytics
  trackPageView(to.path)
})

Route Exclusive Guard ​

Configured via RouteConfig.beforeEnter, only takes effect for that route:

ts
const routes = [
  {
    path: 'pages/admin/admin',
    name: 'admin',
    beforeEnter: (to, from) => {
      if (hasRole('admin')) return true
      return { name: '403' }
    }
  }
]

See RouteConfig.beforeEnter.

Component-level Leave Guard ​

ts
type RouteLeaveGuard = (
  to: RouteLocation,
  from: RouteLocation,
) => NavigationGuardReturn | Promise<NavigationGuardReturn>

Registered via onBeforeRouteLeave(fn) in <script setup>, automatically removed when the component is unmounted:

ts
import { onBeforeRouteLeave } from '@meng-xi/uni-router'

// In <script setup>
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges()) {
    uni.showModal({
      title: 'Confirm',
      content: 'You have unsaved changes. Leave?',
      success: (res) => {
        if (res.confirm) return true
      }
    })
    return false
  }
  return true
})

Back Guard ​

Registered via router.onBeforeBack, executed when a back operation is triggered (App physical back / navigation-bar back / uni.navigateBack, H5 browser back / back gesture, router.back()):

ts
type BackGuardReturn = boolean | void | Promise<boolean | void>

type BackGuard = (to: RouteLocation, from: RouteLocation) => BackGuardReturn
  • Return false to block back; true / undefined to allow
  • Supports async (Promise)
  • After the back guard passes, the beforeEach → beforeResolve chain is reused
ts
router.onBeforeBack((to, from) => {
  if (hasUnsavedChanges()) return false // Block back
  // return undefined or true to allow
})

// Remove the guard
const remove = router.onBeforeBack(guard)
remove()

Platform Support

App wires the back guard via onBackPress, H5 via the popstate event; mini-program native back (capsule/physical key/swipe) cannot be intercepted. For iOS swipe, pair with app.setSideSlipGesture('none') to disable the gesture. See Guards - Back Guard.

Execution Order ​

The guard execution order for a complete navigation:

1. beforeEach (global before guard, in registration order)
   ↓
2. beforeEnter (route exclusive guard, in array order)
   ↓
3. beforeResolve (global resolve guard, in registration order)
   ↓
4. Navigation confirmed, execute uni native jump
   ↓
5. afterEach (global after hook, in registration order)

For back operations (physical back / browser back / router.back()), the order is onBeforeBack → beforeEach → beforeResolve → uni.navigateBack → afterEach, i.e. the back guard runs before the global before guards.

Behavior after guard abort

  • Any guard returns false or throws an error: navigation aborts, subsequent guards don't execute
  • Any guard returns a redirect: restarts the full flow (from beforeEach)
  • afterEach is unaffected: only executes after navigation is confirmed, cannot abort

Controllable Redirect ​

By returning a { location, mode } object, you can simultaneously specify the redirect target and the navigation method used for the redirect:

ts
router.beforeEach((to, from) => {
  if (to.meta.requireAuth && !isLoggedIn()) {
    // Use replace to go to login page, avoiding the login page staying in the page stack
    return { location: { name: 'login', query: { redirect: to.fullPath } }, mode: 'replace' }
  }
})

mode Options ​

modeuni APIUse Case
'push'uni.navigateToNeed to return to original page after login, keep target page in stack
'replace'uni.redirectToReplace current page, no history (e.g. login page)
'relaunch'uni.reLaunchClear stack (e.g. return home on insufficient permissions)

Behavior Rules ​

  • Explicit mode takes priority over the original navigation method
  • When mode is not specified, the original navigation method is used (back falls back to relaunch)
  • location supports path strings, path objects, or named objects

Promise Style Return Value ​

ts
type NavigationGuardReturn = void | undefined | boolean | RouteLocationRaw | NavigationRedirect | Error | null
Return ValueDescription
undefined / voidAllow
nullAllow
trueAllow
falseAbort
RouteLocationRawRedirect (uses original navigation method)
NavigationRedirectRedirect and specify navigation method
ErrorThrow error, abort navigation
ts
// Allow
router.beforeEach(() => {})

// Abort
router.beforeEach(() => false)

// Redirect (uses original navigation method)
router.beforeEach(() => ({ name: 'login' }))

// Redirect (replace method)
router.beforeEach(() => ({ location: { name: 'login' }, mode: 'replace' }))

// Throw error
router.beforeEach(() => {
  return new Error('Insufficient permissions')
})

Practical Examples ​

Login Validation ​

ts
router.beforeEach((to, from) => {
  const isLoggedIn = !!uni.getStorageSync('token')

  if (to.meta.requireAuth && !isLoggedIn) {
    // Redirect to login page, use replace to avoid returning to protected page
    return { name: 'login', query: { redirect: to.fullPath } }
  }

  return true
})

Permission Validation ​

ts
// Type augmentation
declare module '@meng-xi/uni-router' {
  interface RouteMeta {
    roles?: string[]
  }
}

router.beforeEach((to) => {
  if (to.meta.roles) {
    const userRoles = getUserRoles()
    if (!to.meta.roles.some(r => userRoles.includes(r))) {
      uni.showToast({ title: 'No permission', icon: 'none' })
      return false
    }
  }
  return true
})

Async Data Preloading ​

ts
router.beforeResolve(async (to) => {
  if (to.name === 'detail') {
    try {
      await store.fetchDetail(to.query.id)
    } catch (err) {
      uni.showToast({ title: 'Load failed', icon: 'none' })
      return false
    }
  }
  return true
})

Page Analytics ​

ts
declare module '@meng-xi/uni-router' {
  interface RouteMeta {
    trackName?: string
  }
}

router.afterEach((to, from) => {
  if (to.meta.trackName) {
    trackPageView(to.meta.trackName, {
      from: from.path,
      to: to.path,
      duration: Date.now() - pageStartTime
    })
  }
  pageStartTime = Date.now()
})

Dynamic Title ​

ts
router.afterEach((to) => {
  const title = to.meta.title as string | undefined
  uni.setNavigationBarTitle({ title: title || 'Default Title' })
})

Prevent Duplicate Navigation ​

ts
let isNavigating = false

router.beforeEach((to, from) => {
  if (isNavigating) {
    return false
  }
  isNavigating = true
  return true
})

router.afterEach(() => {
  isNavigating = false
})

FAQ ​

Can I access the component instance in a guard? ​

  • beforeEach / beforeResolve: No, the target component hasn't been created yet
  • afterEach: No, but you can get the page instance via getCurrentPages()
  • Component-level onBeforeRouteLeave: No, the component instance is still active but the guard is called before the navigation completes

Note: beforeRouteEnter is not supported. Use onBeforeRouteLeave for component-level leave guard, or use global guards for enter logic.

What happens if an exception is thrown in a guard? ​

The exception will be caught by router.onError and the current navigation will be aborted:

ts
router.onError((err, to, from) => {
  console.error('Navigation error:', err)
  uni.showToast({ title: 'Page load failed', icon: 'none' })
})

router.beforeEach(async (to) => {
  if (to.meta.requireAuth) {
    const user = await fetchUser()  // May throw network error
    if (!user) return { name: 'login' }
  }
  return true
})

Next Steps ​

Released under the MIT License.