@xmachines/play-vue
API / @xmachines/play-vue
Vue 3 renderer for the XMachines Play Architecture — passively observes actor signals and renders UI via
@json-render/vue.
Part of the XMachines Play monorepo.
Overview
@xmachines/play-vue is the Vue 3 rendering layer for XMachines Play. It bridges TC39 Signals (actor state) to Vue reactivity and drives component rendering through @json-render/vue.
Architecture invariants this package upholds:
- Passive Infrastructure — Components observe actor signals; they never decide state transitions.
- Signal-Only Reactivity — TC39 Signals are the source of truth; Vue reactivity is used only to trigger re-renders.
- Actor Authority — The actor controls view selection; the renderer reflects it.
Installation
npm install @xmachines/play-vuePeer dependencies (install alongside):
npm install vue@^3.5.0 xstate@^5.31.0 @xstate/store@^3.17.0 @json-render/vue@^0.18.0 @json-render/core@^0.18.0 @json-render/xstate@^0.18.0Quick Start
<template> <PlayUIProvider :actor="actor" :registryResult="registryResult"> <PlayRenderer /> </PlayUIProvider></template>
<script setup lang="ts">import { defineRegistry, PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";import { definePlayer } from "@xmachines/play-xstate";import { myMachine } from "./machine.js";import { myCatalog } from "./catalog.js";import HomeSFC from "./views/Home.vue";import LoginSFC from "./views/Login.vue";
const createPlayer = definePlayer({ machine: myMachine });const actor = createPlayer();actor.start();
const registryResult = defineRegistry(myCatalog, { components: { Home: HomeSFC, // .vue SFCs are auto-wrapped Login: LoginSFC, }, actions: { login: async (args) => actor.send({ type: "auth.login", ...args }), logout: async () => actor.send({ type: "auth.logout" }), },});</script>API Summary
Components
<PlayUIProvider>
Batteries-included composite provider. Wraps <ActorProvider> and JSONUIProvider in one component. Recommended for most apps.
| Prop | Type | Required | Description |
|---|---|---|---|
actor | AbstractActor & Viewable | ✅ | The XMachines actor instance |
registryResult | DefineRegistryResult | ✅ | Result of defineRegistry() |
store | StateStore | — | External controlled state store (optional) |
onRenderError | RenderErrorHandler | — | Error handler for render failures |
navigate | (path: string) => void | — | Link navigation function |
validationFunctions | Record<string, Function> | — | Custom validation functions |
functions | Record<string, Function> | — | Named functions for $computed expressions |
Slots: default (rendered content), fallback (shown while actor view is null)
<PlayRenderer>
Zero-prop leaf component. Reads the current spec and registry from the nearest <ActorProvider> or <PlayUIProvider> context and renders via <Renderer>. Must be placed inside one of those providers.
<PlayUIProvider :actor="actor" :registryResult="registryResult"> <PlayRenderer /></PlayUIProvider><ActorProvider>
Low-level escape hatch for custom provider composition. Owns the full actor lifecycle — signal subscription, per-view state store, handler resolution, and Vue context provision. Use <PlayUIProvider> unless you need fine-grained control.
| Prop | Type | Required | Description |
|---|---|---|---|
actor | AbstractActor & Viewable | ✅ | The XMachines actor instance |
registryResult | DefineRegistryResult | ✅ | Result of defineRegistry() |
store | StateStore | — | External controlled state store |
onRenderError | RenderErrorHandler | — | Override render error handler |
Functions
defineRegistry(catalog, options)
Drop-in replacement for defineRegistry from @json-render/vue. Always import from @xmachines/play-vue rather than @json-render/vue when working with Vue SFCs — this wrapper automatically detects .vue SFCs in the components map and wraps them via h() so Vue composables (including inject-based ones) work correctly inside <script setup>.
import { defineRegistry } from "@xmachines/play-vue";// NOT: import { defineRegistry } from "@json-render/vue"
import LoginSFC from "./views/Login.vue";import DashboardSFC from "./views/Dashboard.vue";
const registryResult = defineRegistry(catalog, { components: { Login: LoginSFC, // .vue SFC — auto-wrapped via h() Dashboard: DashboardSFC, }, actions: { login: async (args, setState, getState) => { /* ... */ }, },});Plain ComponentFn functions (non-SFC) also work and are passed through unchanged. Mixing SFCs and plain functions in the same registry is supported.
useActor()
Vue composable for accessing the raw actor inside a PlayRenderer tree. Avoids prop drilling for deeply nested components.
import { useActor } from "@xmachines/play-vue";
// Inside a component rendered by PlayRenderer:const actor = useActor();actor.send({ type: "SUBMIT" });Throws if called outside an <ActorProvider> or <PlayUIProvider> tree.
getPlayViewContext()
Access the current ViewContextValue — { spec, handlers, registry, store } — from inside an <ActorProvider> tree.
import { getPlayViewContext } from "@xmachines/play-vue";
// Inside setup() of a component within an ActorProvider tree:const view = getPlayViewContext();// view.spec, view.handlers, view.registry, view.storeRe-exported from @json-render/vue
The following are re-exported so consumers import everything from @xmachines/play-vue:
Components: JSONUIProvider, StateProvider, ActionProvider, VisibilityProvider, ValidationProvider, Renderer
Composables: useBoundProp
Types: JSONUIProviderProps, StateProviderProps, ActionProviderProps, ValidationProviderProps, RendererProps, ComponentFn, ComponentContext, DefineRegistryResult
Testing
Run tests for this package in isolation:
# From the monorepo rootnpm test -w packages/play-vue
# Watch modenpm run test:watch -w packages/play-vue
# With coverage (80% threshold enforced on lines, functions, branches, statements)npx vitest run --coverage --config packages/play-vue/vitest.config.tsTests use Vitest with jsdom environment and @vue/test-utils for component mounting.
License
MIT — see LICENSE.
@xmachines/play-vue - Vue 3 renderer for XMachines Play architecture
Provides a thin Vue rendering layer that passively observes actor signals and renders UI components via @json-render/vue. Vue reactivity is only used to trigger re-renders — signals are the source of truth.
Re-exports defineRegistry (SFC-aware — auto-wraps .vue SFCs via h()),
useBoundProp, ComponentFn, ComponentContext, and all json-render providers
so consumers import everything from @xmachines/play-vue rather than
@json-render/vue directly.
Interfaces
- ActionProviderProps
- ActorProviderProps
- ComponentContext
- DefineRegistryResult
- JSONUIProviderProps
- PlayUIProviderProps
- RendererProps
- StateProviderProps
- ValidationProviderProps
- ViewContextValue
- VisibilityProviderProps
Type Aliases
Variables
- ActionProvider
- JSONUIProvider
- PlayRenderer
- Renderer
- StateProvider
- ValidationProvider
- VisibilityProvider
Functions
References
ActorProvider
Renames and re-exports PlayRenderer
PlayUIProvider
Renames and re-exports PlayRenderer
RenderErrorHandler
Re-exports RenderErrorHandler