Composables
Uni Router provides five composable functions for accessing the router instance, current route, registering component-level guards, obtaining navigation state, and inter-page communication in Vue 3's <script setup>. This chapter covers usage, reactivity principles, and practical tips in detail.
useRouter()
Get the current router instance. Must be called inside a Vue component's setup() function, and the router must be installed via app.use(router) first.
Basic Usage
import { useRouter } from '@meng-xi/uni-router'
const router = useRouter()
// Programmatic navigation
await router.push({ name: 'about' })
await router.replace({ name: 'home' })
await router.back()
await router.relaunch({ name: 'login' })Complete Example
<template>
<view>
<button @click="goAbout">Go to About</button>
<button @click="goBack">Go Back</button>
<button @click="replaceHome">Replace with Home</button>
</view>
</template>
<script setup lang="ts">
import { useRouter } from '@meng-xi/uni-router'
const router = useRouter()
async function goAbout() {
try {
await router.push({ name: 'about', query: { from: 'home' } })
} catch (err) {
console.error('Navigation failed:', err)
}
}
async function goBack() {
try {
await router.back()
} catch (err) {
// Insufficient stack, fall back to home
await router.relaunch({ name: 'home' })
}
}
async function replaceHome() {
await router.replace({ name: 'home' })
}
</script>Error Cases
| Scenario | Error Code | Description |
|---|---|---|
| Called outside setup | SETUP_ERROR | inject can only be used inside setup |
| Router not installed | SETUP_ERROR | Need to call app.use(router) first |
// ❌ Called outside setup
const router = useRouter() // Error
// ✅ Called inside setup
import { useRouter } from '@meng-xi/uni-router'
export default {
setup() {
const router = useRouter() // Correct
return { router }
}
}useRoute()
Get a reactive reference to the current route location. Must be called inside a Vue component's setup() function.
Basic Usage
import { useRoute } from '@meng-xi/uni-router'
const route = useRoute()
// Access via .value in <script setup>
console.log(route.value.path)
console.log(route.value.query)
console.log(route.value.params)
console.log(route.value.meta)<template>
<!-- Auto-unwrapped in template, no .value needed -->
<text>Current path: {{ route.path }}</text>
<text>Query: {{ route.query.id }}</text>
<text>Page params: {{ route.params.id }}</text>
<text>Page title: {{ route.meta.title }}</text>
</template>Reactivity
useRoute() returns Ref<RouteLocation>, which automatically updates when the route changes:
import { useRoute } from '@meng-xi/uni-router'
import { watch } from 'vue'
const route = useRoute()
// Watch route changes
watch(
() => route.value.query.id,
(newId, oldId) => {
console.log('ID changed:', oldId, '→', newId)
if (newId) fetchDetail(newId)
}
)Shared Reactivity
The same router instance shares the same reactive ref, ensuring all components get consistent route state. When the route changes, all components using useRoute() will update.
RouteLocation Details
The RouteLocation returned by useRoute() contains the following fields:
interface RouteLocation {
path: string // Page path
name?: string // Route name
query: Record<string, string> // Query parameters
params: Record<string, any> // Page parameters (complex data)
meta: RouteMeta // Route meta information
fullPath: string // Full path (including query)
_synced?: boolean // Whether it's a state sync (physical back, etc.)
}Query Convenience Methods
query provides type conversion methods that automatically parse strings to the corresponding types:
const route = useRoute()
// Basic access (string)
const id = route.value.query.id // '123'
// Type conversion
const pageNum = route.value.queryInt('page', 1) // 123
const price = route.value.queryNumber('price', 0) // 99.9
const enabled = route.value.queryBool('enabled', false) // true| Method | Return Type | Description |
|---|---|---|
queryInt(key, default?) | number | undefined | Parse as integer |
queryNumber(key, default?) | number | undefined | Parse as floating-point number |
queryBool(key, default?) | boolean | undefined | Parse as boolean |
Default Values
All convenience methods accept a default value parameter. When the query doesn't exist or parsing fails, the default value is returned.
Accessing params
const route = useRoute()
// Access params (complex data)
const item = route.value.params.item as Item
const list = route.value.params.list as Item[]params Limitations
params functionality requires registering ParamsPlugin. Using it without registration throws a PLUGIN_REQUIRED error.
Other limitations:
- params is stored in memory by default and lost on H5 refresh; set
persistent: true(or globalparamsPersistent: true) to persist to storage, surviving refresh - params can be passed with
push/replace/relaunch; only when the target is a TabBar page (switchTabdoesn't support query, so__params_keycan't be passed) can params not be received - See Navigation - Passing Complex Data with params
usePageChannel()
Gets the bidirectional communication channel between the current page and the navigation initiator. Must be called inside a Vue component's setup() function.
Prerequisite
Requires registering ChannelPlugin and setting useUniEventChannel: true:
import { createRouter, ChannelPlugin } from '@meng-xi/uni-router'
const router = createRouter({
routes: [...],
plugins: [ChannelPlugin],
useUniEventChannel: true
})Without ChannelPlugin registered, the events parameter will throw a PLUGIN_REQUIRED error. In the default mode (useUniEventChannel: false), usePageChannel() returns a no-op channel.
Basic Usage
import { usePageChannel } from '@meng-xi/uni-router'
const channel = usePageChannel()
// Listen for events from the initiator
channel.on('init', (data) => {
console.log('Received init data:', data)
})
// Send an event to the initiator
channel.emit('ready', { status: 'loaded' })How It Works
usePageChannel() reads route.params.__navId internally:
- Returns a shared
UniEventChannelinstance (based onuni.$emit/uni.$on) whennavIdis present - Returns a
noopChannelwhennavIdis absent, avoiding null checks - Automatically destroys the channel and cleans up all listeners on page unmount
// Initiator: push/replace/relaunch all return eventChannel
const { eventChannel } = await router.push({
name: 'detail',
params: { id: 123 },
events: {
ready(data) { console.log('Target page ready:', data) }
}
})
eventChannel.emit('init', { message: 'Init data' })<!-- Target page -->
<script setup lang="ts">
import { usePageChannel } from '@meng-xi/uni-router'
const channel = usePageChannel()
channel.on('init', (data) => {
console.log('Received init:', data)
})
channel.emit('ready', { status: 'loaded' })
</script>Sticky Event Caching
The built-in channel implements a sticky event mechanism, solving the timing race where the initiator's emit happens before the target page's setup registers a listener:
emitalways caches the event arguments- When
on/onceregisters a listener and a cache exists, it async-triggers (without deleting the cache) - Regardless of the order of
emitandon, all listeners receive the data from the lastemit
// Even if the initiator's emit runs before the target page's on, the listener still receives data
channel.on('init', (data) => {
// ✅ Receives data (async-triggered via cache)
console.log('Received:', data)
})
channel.once('init', (data) => {
// ✅ Also receives (once manually removes the wrapper when triggered via cache)
console.log('Once only:', data)
})Differences from native getOpenerEventChannel
- Native
getOpenerEventChannel()only works withpush, and events are lost when emit precedes on usePageChannel()supports all navigation methods (requiresuseUniEventChannel: true), and sticky caching prevents event loss__nav_idis persisted in the URL, so the channel can be rebuilt after H5 refresh
See usePageChannel() API and Page Communication for details.
onBeforeRouteLeave()
A component-level leave guard that executes when the current component is about to leave. Controls navigation behavior through return values, consistent with Vue Router 4.x's onBeforeRouteLeave.
Implementation
Internally registers a guard via router.beforeEach, which only executes the user's guard when the from path matches the current component's path. Automatically removed on component unmount, no manual cleanup required.
Basic Usage
import { onBeforeRouteLeave } from '@meng-xi/uni-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges) {
return false // abort navigation
}
// return undefined or true to proceed
})Leave Confirmation Dialog
onBeforeRouteLeave((to, from) => {
if (from.meta.dirty) {
return new Promise((resolve) => {
uni.showModal({
title: 'Confirm',
content: 'You have unsaved changes. Leave anyway?',
success: (res) => resolve(res.confirm ? true : false)
})
})
}
})Notes
| Scenario | Description |
|---|---|
| Guard not registered | No interception, navigation proceeds normally |
| Async guards | Supports async/await and returning Promises |
| Component unmount | Guard is automatically removed, no cleanup needed |
| Global guards | onBeforeRouteLeave guards have lower priority than router.beforeEach global guards |
useLink()
Exposes the internal behavior of RouterLink as a composable, for building custom navigation components. Consistent with Vue Router 4.x's useLink.
Basic Usage
import { useLink } from '@meng-xi/uni-router'
const { href, isActive, isExactActive, navigate } = useLink({
to: { name: 'pagesDetailDetail', query: { id: '1' } }
})
console.log(href.value) // '/pages/detail/detail?id=1'
console.log(isActive.value) // Whether current route matchesCustom Navigation Component
<script setup lang="ts">
import { useLink } from '@meng-xi/uni-router'
import { computed } from 'vue'
const props = defineProps<{
to: RouteLocationRaw
replace?: boolean
activeClass?: string
}>()
const { href, isActive, navigate } = useLink(props)
const classes = computed(() => ({
'nav-link': true,
[props.activeClass || 'active']: isActive.value
}))
</script>
<template>
<view :class="classes" @click="navigate">
<slot />
</view>
</template>Return Value
| Property | Type | Description |
|---|---|---|
route | ComputedRef<RouteLocation> | Resolved route object |
href | ComputedRef<string> | Target path string (fullPath with query) |
isActive | ComputedRef<boolean> | Whether current route matches (by path) |
isExactActive | ComputedRef<boolean> | Whether current route exactly matches (by fullPath) |
navigate | () => Promise<NavigationResult> | Navigate to the target page |
Call Constraints
Must be called inside setup
useLink() internally depends on useRouter() and useRoute(), so it can only be called inside a component's setup() function (or <script setup>).
Using Outside Components
In non-component scenarios like Pinia stores or utility functions, you cannot use useRouter() / useRoute(). In these cases, import the router instance directly:
// router/index.ts
import { createRouter } from '@meng-xi/uni-router'
const router = createRouter({ routes })
export default router// stores/user.ts
import { defineStore } from 'pinia'
import router from '@/router'
export const useUserStore = defineStore('user', () => {
async function login(credentials) {
const { token } = await loginApi(credentials)
uni.setStorageSync('token', token)
// Use router inside store
await router.push({ name: 'home' })
}
function getCurrentPath() {
// Access current route (non-reactive)
return router.currentRoute.path
}
return { login, getCurrentPath }
})Non-reactive
router.currentRoute is a plain property, not reactive. For reactivity, use useRoute() inside components.
Using with Options API
If using the Options API, access via this.$router and this.$route:
<script>
export default {
computed: {
currentPath() {
return this.$route.path
},
pageTitle() {
return this.$route.meta.title
}
},
methods: {
navigate() {
this.$router.push({ name: 'about' })
},
goBack() {
this.$router.back()
}
}
}
</script>Global Property Registration
app.use(router) automatically registers the $router and $route global properties:
// main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import router from './router'
export function createApp() {
const app = createSSRApp(App)
app.use(router) // Registers $router and $route
return { app }
}Practical Tips
1. Encapsulating Navigation Logic
// composables/use-nav.ts
import { useRouter } from '@meng-xi/uni-router'
import { ref } from 'vue'
export function useNav() {
const router = useRouter()
const loading = ref(false)
async function safePush(location) {
loading.value = true
try {
await router.push(location)
} catch (err) {
if (err.code !== 'NAVIGATION_DUPLICATED') {
uni.showToast({ title: 'Navigation failed', icon: 'none' })
console.error(err)
}
} finally {
loading.value = false
}
}
async function safeBack(fallback?) {
const pages = getCurrentPages()
if (pages.length > 1) {
await router.back()
} else if (fallback) {
await router.relaunch(fallback)
} else {
await router.relaunch({ name: 'home' })
}
}
return { loading, safePush, safeBack }
}2. Watching Route Changes
import { useRoute } from '@meng-xi/uni-router'
import { watch, computed } from 'vue'
const route = useRoute()
// Watch path changes
watch(
() => route.value.path,
(newPath) => {
console.log('Page switched:', newPath)
}
)
// Watch query changes
watch(
() => route.value.query,
(newQuery) => {
console.log('Query changed:', newQuery)
},
{ deep: true }
)
// Computed properties
const isLoginPage = computed(() => route.value.name === 'login')
const requireAuth = computed(() => route.value.meta.requireAuth === true)3. Handling Page Parameters
import { useRoute } from '@meng-xi/uni-router'
import { ref, onMounted } from 'vue'
const route = useRoute()
const detail = ref(null)
onMounted(async () => {
// Get ID from query
const id = route.value.queryInt('id', 0)
if (!id) {
uni.showToast({ title: 'Invalid parameter', icon: 'none' })
return
}
// Get complex data from params
const previewData = route.value.params.preview
if (previewData) {
detail.value = previewData // Use preloaded data directly
} else {
detail.value = await fetchDetail(id) // Network request
}
})4. Conditional Rendering
<template>
<view>
<text v-if="route.meta.requireAuth">Login required</text>
<text v-else>Public page</text>
<button v-if="route.name !== 'home'" @click="goHome">Back to Home</button>
</view>
</template>
<script setup>
import { useRoute, useRouter } from '@meng-xi/uni-router'
const route = useRoute()
const router = useRouter()
function goHome() {
router.push({ name: 'home' })
}
</script>Complete Example
<template>
<view class="container">
<text>Current path: {{ route.path }}</text>
<text>Page title: {{ route.meta.title }}</text>
<text>Query params: {{ JSON.stringify(route.query) }}</text>
<text>Page params: {{ JSON.stringify(route.params) }}</text>
<view v-if="loading">Navigating...</view>
<button @click="goAbout">Go to About</button>
<button @click="goBack">Go Back</button>
<button @click="replaceHome">Replace with Home</button>
</view>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from '@meng-xi/uni-router'
import { ref, watch } from 'vue'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
// Watch route changes
watch(
() => route.value.path,
(newPath) => {
console.log('Route changed:', newPath)
}
)
async function goAbout() {
loading.value = true
try {
await router.push({ name: 'about', query: { from: 'home' } })
} catch (err) {
console.error('Navigation failed:', err)
} finally {
loading.value = false
}
}
async function goBack() {
try {
await router.back()
} catch (err) {
// Insufficient stack, go home
await router.relaunch({ name: 'home' })
}
}
async function replaceHome() {
await router.replace({ name: 'home' })
}
</script>Next Steps
- Route Configuration — Detailed route configuration
- Navigation — Navigation API usage
- Plugin System — Learn about plugin registration
- API Reference — Complete API documentation
