Skip to content

@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.

License: MIT Version


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

Terminal window
npm install @xmachines/play-vue

Peer dependencies (install alongside):

Terminal window
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.0

Quick Start

App.vue
<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.

PropTypeRequiredDescription
actorAbstractActor & ViewableThe XMachines actor instance
registryResultDefineRegistryResultResult of defineRegistry()
storeStateStoreExternal controlled state store (optional)
onRenderErrorRenderErrorHandlerError handler for render failures
navigate(path: string) => voidLink navigation function
validationFunctionsRecord<string, Function>Custom validation functions
functionsRecord<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.

PropTypeRequiredDescription
actorAbstractActor & ViewableThe XMachines actor instance
registryResultDefineRegistryResultResult of defineRegistry()
storeStateStoreExternal controlled state store
onRenderErrorRenderErrorHandlerOverride 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.store

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

Terminal window
# From the monorepo root
npm test -w packages/play-vue
# Watch mode
npm 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.ts

Tests 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

Type Aliases

Variables

Functions

References

ActorProvider

Renames and re-exports PlayRenderer


PlayUIProvider

Renames and re-exports PlayRenderer


RenderErrorHandler

Re-exports RenderErrorHandler