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
// ❌ 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
// ❌ Forgot to return a value
router.beforeEach((to, from) => {
if (needAuth) {
return { name: 'login' }
}
// Missing return in else branch
})
// ✅ All branches return a value
router.beforeEach((to, from) => {
if (needAuth) {
return { name: 'login' }
}
// else: pass (return undefined)
})3. Are async guards properly awaited
// ❌ Async operation not awaited, guard resolves before async completes
router.beforeEach((to, from) => {
fetchUser().then(user => {
if (!user) return { name: 'login' }
})
// Returns undefined immediately here, without waiting for fetchUser
})
// ✅ Use async/await for async guards
router.beforeEach(async (to, from) => {
const user = await fetchUser()
if (!user) {
return { name: 'login' }
}
// else: pass (return undefined)
})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.
// ❌ params lost after H5 refresh
const route = useRoute()
console.log(route.params.id) // undefinedSolution: Use query for critical params, params only for complex objects.
// 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().
// ❌ 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) // CorrectPage 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:
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.
Back Cannot Be Intercepted
Symptom
User presses physical back button (Android back, H5 browser back), onBeforeBack guard doesn't trigger.
Cause
Different platforms have different back interception capabilities:
- App: Physical back / navigation-bar back /
uni.navigateBackare wired into the back guard chain via the global mixin'sonBackPress;onBeforeBackworks - H5: Browser back button / back gesture are wired into the back guard chain via the
popstateevent;onBeforeBackworks - Mini-program: Top back arrow, physical back, and swipe back are controlled by the host and cannot be intercepted (only
router.back()/ programmaticuni.navigateBackcan be intercepted) - iOS swipe: Bypasses guards by default; configure
app.setSideSlipGesture('none')to disable
Solution
// Register a global back guard (all platforms, covers App/H5 native back + router.back)
router.onBeforeBack((to, from) => {
if (hasUnsavedData) {
uni.showModal({ title: 'Notice', content: 'You have unsaved changes. Leave anyway?' })
return false // Block back
}
})
// iOS swipe-back bypasses guards by default; disable the gesture per page
const router = createRouter({
routes,
app: {
setSideSlipGesture(to) {
return to.meta.requireLeaveConfirm ? 'none' : 'close'
}
}
})
// Mini-program native back cannot be intercepted; use onRouteChange for after-the-fact handling
router.onRouteChange((to, from) => {
if (to._synced) {
handleBackNavigation(to, from)
}
})switchTab Query Lost
Symptom
await router.push({ name: 'user', query: { tab: 'orders' } })
// Target page route.query.tab is undefinedCause
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:
// 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 syncRoute() state sync, afterEach hook doesn't execute.
Cause
afterEach only triggers after a complete navigation (through the guard chain). syncRoute() only syncs route state and doesn't go through the guard chain, so afterEach doesn't trigger. Physical back / browser back go through the back guard chain, and afterEach triggers normally after the guards pass.
Solution
Use onRouteChange to listen for all route changes (including state sync):
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.
Navigation Deadlock in Guards
Symptom
Calling router.push() in a guard causes navigation to hang.
Cause
// ❌ Deadlock
router.beforeEach((to, from) => {
router.push({ name: 'other' }) // Triggers new navigation, waits for current
// Returns undefined, but current navigation waits for the nested push
// Mutual wait → deadlock
})Solution
Use return location redirect instead of calling router.push in guards:
// ✅ Redirect
router.beforeEach((to, from) => {
if (needRedirect) {
return { name: 'other' } // Redirect, no deadlock
}
// else: pass (return undefined)
})Duplicate Navigation Error
Symptom
await router.push({ name: 'about' })
await router.push({ name: 'about' }) // Throws NavigationFailureCause
push to the same location (path + name + query match) throws NAVIGATION_DUPLICATED error to prevent meaningless duplicate stacking.
Solution
1. Catch and ignore
try {
await router.push({ name: 'about' })
} catch (err) {
if (err.code !== 'NAVIGATION_DUPLICATED') throw err
}2. Use replace instead
await router.replace({ name: 'about' }) // replace doesn't check duplicates3. Wrap safe push
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:
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():
// ✅ 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
// 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:
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
// 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
// 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
// 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
// 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:
// 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
const router = createRouter({
routes,
guardTimeout: 30000 // 30 seconds
})2. Optimize guard logic
Move time-consuming operations to beforeResolve or page onLoad:
// ❌ Time-consuming request in beforeEach
router.beforeEach(async (to, from) => {
await fetchLargeData() // Time-consuming
// pass (return undefined)
})
// ✅ Move to page onLoad
router.beforeEach((to, from) => {
// Only quick checks
// pass (return undefined)
})
// 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:
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:
// Initiating page
const store = useCommStore()
store.setData(payload)
await router.replace({ name: 'target' })
// Target page
const data = store.consumeData()See Recipes - Page Communication.
Navigation White Screen
Symptom
After calling router.push, page is blank.
Troubleshooting
1. Is the route path correct
// ❌ 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
// ❌ 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
// 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
// #endifSee 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
// pages.json
{
"subPackages": [
{
"root": "subpkg",
"pages": [
{ "path": "detail/detail", "style": { "navigationBarTitleText": "Detail" } }
]
}
]
}2. Use subpackage paths in route config
const routes: RouteConfig[] = [
{ path: 'subpkg/detail/detail', name: 'detail' }
]Still Can't Solve?
If none of the above solves your issue:
- Check the API docs to confirm usage
- Check Navigation Flow to understand the internal mechanism
- Check Platform Compatibility to confirm if it's a platform limitation
- 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
