@xmachines/play-vue-router
API / @xmachines/play-vue-router
Vue Router 4.x adapter for XMachines Universal Player Architecture. Bidirectional sync between Vue Router and XMachines state machines using Vue’s reactive primitives.
Part of the xmachines-js monorepo.
Installation
npm install @xmachines/play-vue-routerPeer dependencies:
vue^3.5.0 — Vue runtime@vue/reactivity^3.5.0 — Vue reactivity primitivesvue-router^4.0.0 || ^5.0.0 — Vue Router libraryxstate^5.31.0 — XState v5 state machine runtime
Usage
VueRouterBridge — low-level adapter
VueRouterBridge wires Vue Router’s currentRoute ref to an XMachines actor’s currentRoute signal. Both directions are active: the actor drives the URL, and the URL drives the actor.
import { createRouter, createWebHistory } from "vue-router";import { VueRouterBridge, RouteMap } from "@xmachines/play-vue-router";
// 1. Define routesconst router = createRouter({ history: createWebHistory(), routes: [ { path: "/", name: "home", component: HomePage }, { path: "/profile/:userId", name: "profile", component: ProfilePage }, { path: "/settings/:section?", name: "settings", component: SettingsPage }, ],});
// 2. Create a bidirectional state ID ↔ path mappingconst routeMap = new RouteMap([ { stateId: "home", path: "/" }, { stateId: "profile", path: "/profile/:userId" }, { stateId: "settings", path: "/settings/:section?" },]);
// 3. Start the bridge after the router is readyawait router.isReady();const bridge = new VueRouterBridge(router, actor, routeMap);bridge.connect();
// 4. Dispose when tearing down (e.g. onUnmounted)bridge.dispose();PlayRouterProvider — Vue component wrapper
PlayRouterProvider is a convenience component that manages the bridge lifecycle automatically — it calls bridge.connect() after router.isReady() on mount and bridge.disconnect() on unmount.
<script setup lang="ts">import { markRaw } from "vue";import { useRouter } from "vue-router";import { PlayRouterProvider, RouteMap } from "@xmachines/play-vue-router";
const router = useRouter();const routeMap = new RouteMap([ { stateId: "home", path: "/" }, { stateId: "profile", path: "/profile/:userId" },]);
// markRaw prevents Vue from wrapping the actor in a reactive proxy,// which would break TC39 Signal receivers.const actor = markRaw(createActor());</script>
<template> <PlayRouterProvider :actor="actor" :router="router" :routeMap="routeMap" :renderer="(actor, router) => h(AppShell, { actor, router })" /></template>Sending route events from components
<script setup>import { inject } from "vue";
const actor = inject("actor");
function viewProfile(userId) { actor.send({ type: "play.route", to: "#profile", params: { userId } });}</script>
<template> <button @click="viewProfile('123')">View Profile</button></template>API Reference
VueRouterBridge
Implements the RouterBridge protocol by watching Vue Router’s currentRoute shallowRef and the actor’s currentRoute TC39 Signal.
class VueRouterBridge { constructor(vueRouter: Router, actor: RoutableActor, routeMap: RouteMap); connect(): void; disconnect(): void; dispose(): void; // alias for disconnect()}Constructor parameters:
| Parameter | Type | Description |
|---|---|---|
vueRouter | Router | Vue Router instance from createRouter() |
actor | RoutableActor | XMachines actor with a currentRoute signal |
routeMap | RouteMap | Bidirectional state ID ↔ path mapping |
Methods:
connect()— Start bidirectional synchronization. Performs an initial sync from the router’s current path to the actor (cold-load / direct-URL support). Useswatch(router.currentRoute, …)from@vue/reactivity(not@vue/runtime-core) so watcher errors propagate directly without being swallowed by Vue’s global error handler.disconnect()— Stop all watchers and stop the Vue effect scope.dispose()— Alias fordisconnect(), intended foronUnmounted(() => bridge.dispose()).
PlayRouterProvider
Vue component that wraps VueRouterBridge in component lifecycle hooks.
import type { PlayActor } from "@xmachines/play-vue-router";
defineComponent({ name: "PlayRouterProvider", props: { actor: { type: Object as PropType<PlayActor>, required: true }, routeMap: { type: Object as PropType<RouteMap>, required: true }, router: { type: Object as PropType<Router>, required: true }, renderer: { type: Function as PropType<(actor: PlayActor, router: Router) => VNodeChild>, required: true, }, },});The actor prop requires PlayActor (AbstractActor & Routable & Viewable) — the provider renders the current view spec in addition to synchronizing routes. The renderer callback receives the same concrete actor type.
RouteMap / VueRouteMap
RouteMap (re-exported from @xmachines/play-router) is the bidirectional state ID ↔ path mapping used by the bridge. VueRouteMap is an alias for RouteMap — both are identical.
import { RouteMap, createRouteMap } from "@xmachines/play-vue-router";
// Explicit constructionconst routeMap = new RouteMap([ { stateId: "home", path: "/" }, { stateId: "profile", path: "/profile/:userId" }, { stateId: "settings", path: "/settings/:section?" },]);
// Or derive from an XState machineimport { createRouteMap } from "@xmachines/play-router";const routeMap = createRouteMap(machine);Exported error classes (@xmachines/play-vue-router/errors)
All runtime errors extend PlayError from @xmachines/play and are available from the ./errors subpath:
import { VueRouterCorrectionError, VueRouterNavigationError, VueRouterSendError,} from "@xmachines/play-vue-router/errors";| Class | Error code | When thrown |
|---|---|---|
VueRouterCorrectionError | PLAY_VUE_ROUTER_CORRECTION_FAILED | router.replace() rejected when syncing actor → router (correction) |
VueRouterNavigationError | PLAY_VUE_ROUTER_NAV_FAILED | router.push() rejected (navigation guard cancellation, redirect) |
VueRouterSendError | PLAY_VUE_ROUTER_SEND_FAILED | Vue Router watcher callback fails to deliver play.route to the actor |
Each class carries a cause property with the original Vue Router error.
Exported types
// Bridge-level (routing only) — from @xmachines/play-router:export type { RouteMapping, PlayRouteEvent, RouterBridge, RoutableActor,} from "@xmachines/play-router";
// Provider-level (routing + view rendering) — PlayActor re-exported from @xmachines/play-router:export type { PlayActor } from "@xmachines/play-vue-router";// RoutableActor is also exported as a deprecated alias for PlayActorPlayActor is AbstractActor<AnyActorLogic> & Routable & Viewable — the shape required by PlayRouterProvider, which renders the current view spec in addition to synchronizing routes. Use RoutableActor from @xmachines/play-router when only routing is needed (e.g. constructing VueRouterBridge directly).
Architecture
Sync directions
Router → Actor (watch(router.currentRoute, …)):
- User navigates (link click, browser back, programmatic
router.push). - Vue’s
currentRouteshallowRef is assigned a new object. watchfrom@vue/reactivityfires synchronously (scheduler: (job) => job()).- Bridge sanitizes the path, looks up the state ID in
routeMap. - Bridge sends
{ type: "play.route", to: "#stateId", params, query }to the actor.
Actor → Router (TC39 Signal watcher):
- Actor transitions;
actor.currentRoutesignal updates to a new state ID or path. - Signal watcher fires in a microtask.
- Bridge resolves the navigation path via
resolveNavigationPath. - Parameterized patterns without concrete params are skipped (returns
null). - Bridge calls
router.push(resolvedPath).
Echo suppression
lastSyncedPath is set before every router.push() call. When the Vue watcher subsequently fires with the same path (the router echoing the actor-initiated push), the sanitizedPath === lastSyncedPath check short-circuits before any event is sent.
Vue effect scope
The watch watcher runs inside a dedicated effectScope(). Calling disconnect() / dispose() calls scope.stop(), fully removing the watcher without leaking into the global Vue effect scope.
Testing
# Run all tests for this packagenpm test -w packages/play-vue-router
# Watch modenpm run test:watch -w packages/play-vue-router
# With coverage (80 % threshold on lines/functions/branches/statements)npm run test:coverage -w packages/play-vue-routerTest files use Vitest with jsdom and @vue/test-utils. Integration tests (test/integration.test.ts) use real Vue Router instances with SFC fixtures.
Classes
Interfaces
Type Aliases
Variables
Functions
References
VueRouteMap
Renames and re-exports RouteMap