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.