Skip to content

FAQ

A collection of high-frequency issues and troubleshooting ideas when using Uni Router.

Guards Not Working

Symptom

Configured beforeEach, but guard logic doesn't execute.

Troubleshooting

1. Are you calling through the router

ts
// ❌ Direct uni API call, guard doesn't work
uni.navigateTo({ url: '/pages/about/about' })

// ✅ Through the router
await router.push({ name: 'about' })

Solution: Enable interceptUniApi: true to intercept all native API calls.

2. Is the guard returning correctly

ts
// ❌ Forgot to call next
router.beforeEach((to, from, next) => {
  if (needAuth) {
    next({ name: 'login' })
  }
  // Missing next() in else branch
})

// ✅ All branches call next
router.beforeEach((to, from, next) => {
  if (needAuth) {
    next({ name: 'login' })
  } else {
    next()
  }
})

3. Are async guards properly awaited

ts
// ❌ Async operation not awaited, next called before async completes
router.beforeEach((to, from, next) => {
  fetchUser().then(user => {
    if (!user) next({ name: 'login' })
  })
  next() // Executes immediately here
})

// ✅ Use async/await for async guards
router.beforeEach(async (to, from, next) => {
  const user = await fetchUser()
  if (!user) {
    next({ name: 'login' })
  } else {
    next()
  }
})

params Lost

Symptom

After router.push({ path: 'detail', params: { id: 123 } }), the target page's route.params is empty.

Cause

Uni Router's params are implemented via in-memory storage + URL key. Possible causes:

1. Page Refresh (H5)

params are stored in memory; H5 refresh loses memory.

ts
// ❌ params lost after H5 refresh
const route = useRoute()
console.log(route.params.id) // undefined

Solution: Use query for critical params, params only for complex objects.

ts
// Critical params via query
await router.push({ path: 'detail', query: { id: '123' } })

// Complex objects via params (accept refresh loss)
await router.push({
  path: 'detail',
  query: { id: '123' },
  params: { detailData: largeObject }
})

2. Page Stack Cleared

After relaunch or stack overflow, the original page is destroyed and params are cleaned. Note: back() returning to the original page does not lose params (the actual navigation URL preserves __params_key, params can be rebuilt).

3. Wrong Read Timing

params are ready at page onLoad, but need to be read via useRoute().

ts
// ❌ Directly access global variable
import { router } from '@/router'
console.log(router.currentRoute.params.id) // May not be updated

// ✅ Use useRoute
import { useRoute } from '@meng-xi/uni-router'
const route = useRoute()
console.log(route.params.id) // Correct

Page Stack Overflow

Symptom

Navigation error in mini-program: navigateTo:fail webview count limit exceed.

Cause

Mini-program page stack limit is 10 levels; navigateTo fails when exceeded.

Solution

Use useSafeNav wrapper, switch to relaunch when near limit:

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

const router = useRouter()

async function safePush(location) {
  const pages = getCurrentPages()
  if (pages.length >= 8) {
    await router.relaunch(location)
  } else {
    await router.push(location)
  }
}

See Recipes - Page Stack Depth Management.

Physical Back Cannot Be Intercepted

Symptom

User presses physical back button (Android back, mini-program top arrow), guard doesn't trigger.

Cause

This is a uni-app platform limitation:

  • App: Can be intercepted via onBackPress
  • H5: Browser back button cannot be intercepted; can only listen to popstate for after-the-fact handling
  • Mini-program: Top back arrow cannot be intercepted

Solution

ts
// App: onBackPress
import { onBackPress } from '@dcloudio/uni-app'

onBackPress(() => {
  if (hasUnsavedData) {
    showConfirmDialog()
    return true // Block default back
  }
  return false
})

// All platforms: onRouteChange for after-the-fact handling
// currentRoute is auto-synced by the global mixin in install(), no manual syncRoute needed
import { useRouter } from '@meng-xi/uni-router'

const router = useRouter()

router.onRouteChange((to, from) => {
  if (to._synced) {
    // State sync (may be triggered by physical back)
    handleBackNavigation(to, from)
  }
})

See Platform Compatibility.

switchTab Query Lost

Symptom

ts
await router.push({ name: 'user', query: { tab: 'orders' } })
// Target page route.query.tab is undefined

Cause

uni.switchTab doesn't support query params. When target route has meta.isTab: true, the router calls switchTab, and query is discarded.

Solution

Use global state to pass params:

ts
// Initiating page
const tabStore = useTabStore()
tabStore.setTabParam('user', { tab: 'orders' })
await router.push({ name: 'user' })

// Target page onShow
onShow(() => {
  // currentRoute is auto-synced by the global mixin, no manual syncRoute needed
  const param = tabStore.getTabParam('user')
  if (param?.tab) {
    activeTab.value = param.tab
    tabStore.clearTabParam('user')
  }
})

See Recipes - TabBar Page Data Passing.

afterEach Not Triggered

Symptom

After physical back, afterEach hook doesn't execute.

Cause

Physical back is uni-app native behavior; it doesn't go through the router's navigation flow, so afterEach doesn't trigger.

Solution

Use onRouteChange to listen for all route changes (including state sync):

ts
router.onRouteChange((to, from) => {
  console.log('Route change:', from.path, '→', to.path)
  // Physical back also triggers
})

// Distinguish complete navigation from state sync
router.onRouteChange((to, from) => {
  if (to._synced) {
    // State sync (physical back etc.)
  } else {
    // Complete navigation
  }
})

See Navigation Flow - State Sync Mechanism.

Symptom

Calling router.push() in a guard causes navigation to hang.

Cause

ts
// ❌ Deadlock
router.beforeEach((to, from, next) => {
  router.push({ name: 'other' }) // Triggers new navigation, waits for current
  next()                          // Current navigation waits for guard to complete
  // Mutual wait → deadlock
})

Solution

Use next(location) redirect instead of calling router.push in guards:

ts
// ✅ Redirect
router.beforeEach((to, from, next) => {
  if (needRedirect) {
    next({ name: 'other' }) // Redirect, no deadlock
  } else {
    next()
  }
})

Duplicate Navigation Error

Symptom

ts
await router.push({ name: 'about' })
await router.push({ name: 'about' }) // Throws NavigationFailure

Cause

push to the same location (path + name + query match) throws NAVIGATION_DUPLICATED error to prevent meaningless duplicate stacking.

Solution

1. Catch and ignore

ts
try {
  await router.push({ name: 'about' })
} catch (err) {
  if (err.code !== 'NAVIGATION_DUPLICATED') throw err
}

2. Use replace instead

ts
await router.replace({ name: 'about' }) // replace doesn't check duplicates

3. Wrap safe push

ts
async function safePush(location) {
  try {
    await router.push(location)
  } catch (err) {
    if (err.code === 'NAVIGATION_DUPLICATED') return
    throw err
  }
}

H5 Refresh 404

Symptom

In H5, accessing https://example.com/pages/about/about returns 404 after refresh.

Cause

Uni Router uses hash mode (#/path), but users may directly access URLs without hash.

Solution

1. Use hash mode

Ensure URL is like https://example.com/#/pages/about/about; hash is preserved after refresh.

2. Server redirect configuration

If using history mode, configure server to redirect all paths to index.html:

nginx
location / {
  try_files $uri $uri/ /index.html;
}

uni-app H5 Limitation

uni-app's H5 routing is controlled by manifest.json's h5.router.mode; Uni Router works on top of it. Recommend keeping uni-app's hash mode to avoid refresh issues.

TabBar Page onShow Not Triggered

Symptom

Switching from TabBar page A to TabBar page B, then back to A, A's onShow doesn't trigger.

Troubleshooting

1. Are you using switchTab

switchTab should trigger onShow normally when switching TabBar pages. If not triggered, check if reLaunch was used instead.

2. Did you read the correct currentRoute in onShow

The router registers a global mixin in install() that automatically calls syncRoute() in each page's onShow, no manual call needed. Just read via useRoute():

ts
// ✅ currentRoute is auto-synced by the mixin
import { onShow } from '@dcloudio/uni-app'
import { useRoute } from '@meng-xi/uni-router'

const route = useRoute()

onShow(() => {
  console.log(route.value.path, route.value.query)
  // Other logic
})

If you need route info in onLoad (earlier than onShow), you can manually call router.syncRoute() once.

3. Was the page destroyed

reLaunch destroys all pages. If using reLaunch to switch tabs, onShow won't trigger (page is a new instance, triggers onLoad).

Router Initialization Timing

Symptom

Accessing router.currentRoute in App.vue's onLaunch returns initial value instead of current page.

Cause

The router initializes during App.vue's setup phase, when the page stack may not be established yet.

Solution

ts
// App.vue
import { onLaunch } from '@dcloudio/uni-app'
import { useRouter } from '@meng-xi/uni-router'

const router = useRouter()

onLaunch(() => {
  // Router is initialized here, but page stack may not be ready
  console.log(router.currentRoute) // Initial value
})

// The router registers a global mixin in install() that auto-syncs in each page's onShow
// So page-level onShow doesn't need to manually call router.syncRoute()

Cold Start Guard Check

If you need to run guards against the real entry page in onLaunch, pass options.path:

ts
onLaunch((options) => {
  router.isReady().then(() => {
    const launchPath = options?.path ? `/${options.path}` : undefined
    router.guardRoute(launchPath, {
      onAbort: (failure) => {
        router.relaunch({ name: 'home' })
      }
    })
  })
})

Calling guardRoute(undefined) directly will check START_LOCATION (path /) instead of the real entry page. See Router Instance - guardRoute() for details.

Multiple Router Instance Conflict

Symptom

Console warning: Another router instance has already installed interceptors. Replacing with the new instance.

Cause

More than one router instance with interceptUniApi: true enabled. Interceptors are global; only one active instance can exist.

Solution

1. Ensure singleton

ts
// router/index.ts
let router: Router | null = null

export function useAppRouter() {
  if (!router) {
    router = createRouter({
      routes,
      interceptUniApi: true
    })
  }
  return router
}

2. Unload on HMR hot reload

ts
// vite.config.ts
if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    removeInterceptors()
  })
}

Type Extension Not Working

Symptom

After extending RouteMeta, TypeScript doesn't recognize new fields.

Troubleshooting

1. Is the module declared correctly

ts
// types/router.d.ts
import '@meng-xi/uni-router'

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

2. Does tsconfig include this file

json
// tsconfig.json
{
  "include": ["src/**/*.ts", "src/types/**/*.d.ts"]
}

3. Are there multiple conflicting declarations

Check if multiple files declare RouteMeta, which may cause conflicts.

params Lost After back

Symptom

After router.back(), the target page's route.params is empty.

Cause

When back() returns to the original page, since matcher.resolve removes __params_key during resolution, the actual navigation URL doesn't contain the key, preventing syncCurrentRoute from rebuilding params.

Solution

Fixed

This issue has been fixed in the latest version. During push / replace, the actual navigation URL preserves __params_key (even though route.query doesn't expose it). When back() returns, syncCurrentRoute reads the key from URL and uses peek to rebuild params. Usually no manual handling needed.

If params are still lost, possible causes:

1. Page was destroyed

After relaunch or stack overflow, the original page is destroyed and params are cleaned.

2. Cross-relaunch passing

relaunch clears the page stack; params cannot be preserved. Use global state:

ts
// Initiating page
const store = useDataStore()
store.setData(largeObject)
await router.relaunch({ name: 'target' })

// Target page
const data = store.consumeData()

3. TabBar pages

Since switchTab doesn't support query, __params_key cannot be passed, so TabBar pages cannot receive params. Use global state instead.

Guard Timeout

Symptom

Console error: Navigation guard timeout.

Cause

Guard has time-consuming operations (like network requests) exceeding default timeout (10 seconds).

Solution

1. Increase timeout

ts
const router = createRouter({
  routes,
  guardTimeout: 30000 // 30 seconds
})

2. Optimize guard logic

Move time-consuming operations to beforeResolve or page onLoad:

ts
// ❌ Time-consuming request in beforeEach
router.beforeEach(async (to, from, next) => {
  await fetchLargeData() // Time-consuming
  next()
})

// ✅ Move to page onLoad
router.beforeEach((to, from, next) => {
  // Only quick checks
  next()
})

// In page
onLoad(async () => {
  await fetchLargeData()
})

EventChannel Unavailable in replace

Symptom

After router.replace, target page's getOpenerEventChannel() returns undefined.

Cause

EventChannel is a navigateTo-exclusive capability; redirectTo / reLaunch don't support it.

Solution

Option 1 (Recommended): Enable useUniEventChannel: true so all navigation methods support the built-in communication channel:

ts
const router = createRouter({
  routes,
  useUniEventChannel: true
})

// Communicate even after replace
const { eventChannel } = await router.replace({ name: 'target' })
eventChannel.emit('init', { payload })

// Target page receives via usePageChannel()
import { usePageChannel } from '@meng-xi/uni-router'
const channel = usePageChannel()
channel.on('init', (data) => { /* ... */ })

Option 2: Use global state instead:

ts
// Initiating page
const store = useCommStore()
store.setData(payload)
await router.replace({ name: 'target' })

// Target page
const data = store.consumeData()

See Recipes - Page Communication.

Symptom

After calling router.push, page is blank.

Troubleshooting

1. Is the route path correct

ts
// ❌ Wrong path
await router.push({ path: 'about' }) // Missing pages/ prefix

// ✅ Correct path
await router.push({ path: 'pages/about/about' })
// Or use name
await router.push({ name: 'about' })

2. Is the page registered in pages.json

uni-app requires all pages to be registered in pages.json. Unregistered pages cannot be navigated to.

3. Are there JS errors

Check if the target page's onLoad / setup has errors.

4. Is the route config correct

ts
// ❌ path doesn't match pages.json
{ path: 'pages/about', name: 'about' } // Should be pages/about/about

// ✅ Matches pages.json
{ path: 'pages/about/about', name: 'about' }

Animation Not Working

Symptom

Set animation: { type: 'fade-in' }, but no animation effect.

Cause

Animation is only supported on App; H5 and mini-programs don't support it.

Solution

ts
// Conditionally set animation
// #ifdef APP-PLUS
await router.push({ name: 'about', animation: { type: 'fade-in', duration: 300 } })
// #endif

// #ifndef APP-PLUS
await router.push({ name: 'about' }) // No animation on other platforms
// #endif

See Platform Compatibility - Navigation Animation.

Route Lazy Loading

Symptom

Want to load page components on demand to reduce initial bundle size.

Note

uni-app's page loading is determined by pages.json configuration; it doesn't support Vue Router's () => import() lazy loading syntax. All pages registered in pages.json will be bundled.

Alternatives

1. Subpackage loading

json
// pages.json
{
  "subPackages": [
    {
      "root": "subpkg",
      "pages": [
        { "path": "detail/detail", "style": { "navigationBarTitleText": "Detail" } }
      ]
    }
  ]
}

2. Use subpackage paths in route config

ts
const routes: RouteConfig[] = [
  { path: 'subpkg/detail/detail', name: 'detail' }
]

Still Can't Solve?

If none of the above solves your issue:

  1. Check the API docs to confirm usage
  2. Check Navigation Flow to understand the internal mechanism
  3. Check Platform Compatibility to confirm if it's a platform limitation
  4. Submit an issue on GitHub Issues with:
    • Reproduction steps
    • Platform used (App / H5 / mini-program)
    • uni-app version and Uni Router version
    • Complete error information

Released under the MIT License.