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 NavigationGuard = (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
  next: NavigationGuardNext
) => void | Promise<void | boolean | RouteLocationRaw>

Parameters

ParameterTypeDescription
toRouteLocationNormalizedThe target route being navigated to
fromRouteLocationNormalizedThe current route being navigated away from
nextNavigationGuardNextCallback function that controls the navigation flow

Return Value

Guards support two writing styles:

  1. Callback style: Control via next()
  2. Promise style: Return a value directly, the router handles it automatically
ts
// Callback style
router.beforeEach((to, from, next) => {
  if (isLoggedIn()) next()
  else next({ name: 'login' })
})

// Promise style (recommended)
router.beforeEach(async (to, from) => {
  if (isLoggedIn()) return true
  return { name: 'login' }
})

Recommended to use Promise style

  • Better fits async/await style, cleaner code
  • Avoids forgetting to call next() causing navigation to hang
  • Exceptions automatically convert to next(false), no try-catch needed

Callback function that controls the navigation flow, supporting multiple call forms:

ts
type NavigationGuardNext = {
  (): void                                              // Allow
  (valid: boolean): void                                // true=allow, false=abort
  (location: RouteLocationRaw): void                    // Redirect
  (location: RouteLocationRaw, options: NavigationGuardNextOptions): void  // Redirect + options
  (error: Error): void                                  // Throw error
  (cb: (vm: ComponentPublicInstance) => void): void     // Access component instance
}

Allow

ts
next()        // Callback style
return true   // Promise style

Abort

Abort navigation, stay on the current page:

ts
next(false)        // Callback style
return false       // Promise style

Redirect

Redirect to another route:

ts
// Basic redirect (uses push mode by default)
next({ name: 'login' })
next('/pages/login/login')
next({ path: '/pages/login/login', query: { redirect: to.fullPath } })

// Promise style
return { name: 'login' }

Redirect + Options (v1.7.0+)

Control the redirect mode via NavigationGuardNextOptions:

ts
interface NavigationGuardNextOptions {
  mode?: NavigationRedirectMode  // 'push' | 'replace' | 'relaunch'
}

type NavigationRedirectMode = 'push' | 'replace' | 'relaunch'
ts
// Replace mode: don't keep current page in stack after redirect
next({ name: 'login' }, { mode: 'replace' })

// Relaunch mode: clear page stack
next({ name: 'home' }, { mode: 'relaunch' })

// Promise style (via return object)
return {
  location: { name: 'login' },
  mode: 'replace'
}

mode use cases

  • push (default): Regular redirect, keeps current page in stack
  • replace: Login redirect (avoids returning to protected page), jump after form submission
  • relaunch: Logout, switch user, return to home

See Guard Redirect Mode.

Throw Error

ts
next(new Error('Insufficient permissions'))
// Promise style
throw new Error('Insufficient permissions')

The error will be caught by router.onError and navigation will be aborted.

Access Component Instance

ts
next((vm) => {
  // vm is the target page component instance
  vm.fetchData()
})

Limitation

This form is only supported by the beforeRouteEnter guard (rarely used in uni-app because pages are independent components). beforeEach / beforeResolve / afterEach do not support it.

Guard Type Classification

Global Before Guard

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

// 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, does not accept next parameter, 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, next) => {
      if (hasRole('admin')) next()
      else next({ name: '403' })
    }
  }
]

See RouteConfig.beforeEnter.

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)

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

Promise Style Return Value

ts
type GuardResult = void | boolean | RouteLocationRaw | {
  location: RouteLocationRaw
  mode?: NavigationRedirectMode
}
Return ValueEquivalent to next()Description
undefined / voidnext()Allow
truenext()Allow
falsenext(false)Abort
RouteLocationRawnext(RouteLocationRaw)Redirect (push)
{ location, mode }next(location, { mode })Redirect + mode control
ts
// Allow
router.beforeEach(() => {})

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

// Redirect (push)
router.beforeEach(() => ({ name: 'login' }))

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

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 {
      location: { name: 'login', query: { redirect: to.fullPath } },
      mode: 'replace'
    }
  }

  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, next) => {
  if (isNavigating) {
    next(false)
    return
  }
  isNavigating = true
  next()
})

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

FAQ

What happens if I forget to call next()?

After the guard times out (default 10 seconds, configurable via guardTimeout), it automatically aborts navigation and outputs a warning:

[uni-router] Guard timeout after 10000ms, navigation aborted

Solutions

  • Use Promise style to avoid forgetting to call next()
  • Adjust guardTimeout for async operations
  • Check that all branches in the guard call next()

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()
  • beforeRouteEnter: Can access via next((vm) => {...})

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.