Skip to content

Composables

Uni Router provides three composable functions for accessing the router instance, current route, and inter-page communication channel 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?)numberParse as integer
queryNumber(key, default?)numberParse as floating-point number
queryBool(key, default?)booleanParse 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

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 enabling the built-in communication manager via createRouter({ useUniEventChannel: true }). In default mode, it returns a no-op channel (calling on / emit has no effect).

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.

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.