Skip to content

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 ​

ts
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 ​

vue
<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 ​

ScenarioError CodeDescription
Called outside setupSETUP_ERRORinject can only be used inside setup
Router not installedSETUP_ERRORNeed to call app.use(router) first
ts
// ❌ 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 ​

ts
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)
vue
<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:

ts
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:

ts
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:

ts
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
MethodReturn TypeDescription
queryInt(key, default?)number | undefinedParse as integer
queryNumber(key, default?)number | undefinedParse as floating-point number
queryBool(key, default?)boolean | undefinedParse 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 ​

ts
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 global paramsPersistent: true) to persist to storage, surviving refresh
  • params can be passed with push / replace / relaunch; only when the target is a TabBar page (switchTab doesn't support query, so __params_key can'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:

ts
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 ​

ts
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 UniEventChannel instance (based on uni.$emit / uni.$on) when navId is present
  • Returns a noopChannel when navId is absent, avoiding null checks
  • Automatically destroys the channel and cleans up all listeners on page unmount
ts
// 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' })
vue
<!-- 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:

  • emit always caches the event arguments
  • When on / once registers a listener and a cache exists, it async-triggers (without deleting the cache)
  • Regardless of the order of emit and on, all listeners receive the data from the last emit
ts
// 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 with push, and events are lost when emit precedes on
  • usePageChannel() supports all navigation methods (requires useUniEventChannel: true), and sticky caching prevents event loss
  • __nav_id is 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 ​

ts
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 ​

ts
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 ​

ScenarioDescription
Guard not registeredNo interception, navigation proceeds normally
Async guardsSupports async/await and returning Promises
Component unmountGuard is automatically removed, no cleanup needed
Global guardsonBeforeRouteLeave guards have lower priority than router.beforeEach global guards

Exposes the internal behavior of RouterLink as a composable, for building custom navigation components. Consistent with Vue Router 4.x's useLink.

Basic Usage ​

ts
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 matches

Custom Navigation Component ​

vue
<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 ​

PropertyTypeDescription
routeComputedRef<RouteLocation>Resolved route object
hrefComputedRef<string>Target path string (fullPath with query)
isActiveComputedRef<boolean>Whether current route matches (by path)
isExactActiveComputedRef<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:

ts
// router/index.ts
import { createRouter } from '@meng-xi/uni-router'

const router = createRouter({ routes })

export default router
ts
// 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:

vue
<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:

ts
// 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 ​

ts
// 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 ​

ts
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 ​

ts
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 ​

vue
<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 ​

vue
<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 ​

Released under the MIT License.