Skip to content

Navigation Guards

Navigation guards let you run logic before a route becomes active. Use them to block access, redirect users, or run side effects after a transition completes.

How guards work

Each route accepts two hooks:

  • beforeEnter, runs before the route renders. Return false to block navigation, or a path string to redirect.
  • afterEnter, runs after navigation succeeds. Use it for analytics, logging, or focus management.

Both hooks receive the target path as their only argument.

Adding a guard

Pass beforeEnter when registering a route:

·CODE
import { Router } from '@termuijs/router'

const router = new Router({ initialPath: '/' })

router.addRoute('/admin', () => AdminScreen, undefined, {
  beforeEnter: (to) => {
    if (!isAuthenticated()) {
      return '/login'   // redirect to login
    }
    return true         // allow navigation
  },
})

You can also pass guards via addRoutes:

·CODE
router.addRoutes([
  {
    path: '/dashboard',
    component: () => DashboardScreen,
    beforeEnter: (to) => {
      if (!hasPermission('dashboard')) return false
      return true
    },
    afterEnter: (to) => {
      logPageView(to)
    },
  },
])

Guard return values

Return valueEffect
trueAllow navigation to proceed
falseBlock navigation; the current screen stays visible
'/some/path'Redirect to the given path instead

Returning anything other than false or a string is treated as allowing navigation.

Async guards

beforeEnter is typed as (to: string) => boolean | string, so it runs synchronously. For async checks like token validation or API calls, resolve the result before registering the route or cache the auth state in a variable your guard reads from:

·CODE
let authed = false

// Resolve auth state before setting up routes
async function bootstrap() {
  authed = await checkSession()

  router.addRoute('/profile', () => ProfileScreen, undefined, {
    beforeEnter: () => {
      if (!authed) return '/login'
      return true
    },
  })

  router.push('/')
}

If your app has an auth store or reactive state object, read from it directly inside beforeEnter. The guard runs on every navigation attempt, so it always sees the latest value.

Redirecting from a guard

Return a path string to redirect. The router resolves the new path through the full navigation lifecycle, including any guards on the redirect target:

·CODE
router.addRoute('/old-settings', () => SettingsScreen, undefined, {
  beforeEnter: () => '/settings',   // always redirect
})

router.addRoute('/settings', () => SettingsScreen, undefined, {
  beforeEnter: (to) => {
    if (!isAuthenticated()) return '/login'
    return true
  },
})

The router stops redirect chains after 10 hops and emits an error event.

Running logic after navigation

Use afterEnter for side effects that should run once the screen is active:

·CODE
router.addRoute('/reports', () => ReportsScreen, undefined, {
  afterEnter: (to) => {
    analytics.track('screen_view', { path: to })
  },
})

afterEnter always receives the resolved path. It does not affect navigation and has no return value.

Error handling in guards

If a guard throws, the error propagates to the caller. Wrap guard logic in try/catch when the check depends on external state that could fail:

·CODE
router.addRoute('/data', () => DataScreen, undefined, {
  beforeEnter: (to) => {
    try {
      return hasAccess(to) ? true : '/forbidden'
    } catch {
      return '/error'
    }
  },
})

API reference

HookSignatureDescription
beforeEnter(to: string) => boolean | stringRuns before the route renders. Return false to block, a string to redirect, or true to allow.
afterEnter(to: string) => voidRuns after successful navigation. No return value.