Skip to content

@xmachines/play-vue

API / @xmachines/play-vue

Vue 3 renderer for the XMachines Play Architecture. It observes the actor signals and renders the UI through @xmachines/json-render-vue.

License: MIT Version


Overview

@xmachines/play-vue is the Vue 3 rendering layer of XMachines Play. It connects the TC39 Signals (the actor state) to the Vue reactivity, and it renders the components through @xmachines/json-render-vue.

The architecture invariants that this package keeps:

  • Passive Infrastructure — the components observe the actor signals. They never decide a state transition.
  • Signal-Only Reactivity — the TC39 Signals are the source of truth. Vue reactivity only triggers the re-render.
  • Actor Authority — the actor controls the view selection. The renderer reflects it.

Installation

Terminal window
pnpm add @xmachines/play-vue

Peer dependencies. Install them with the package:

Terminal window
pnpm add vue@^3.5.0 xstate@^5.31.0 @xstate/store@^3.17.0 @xmachines/json-render-vue@^0.20.0-xm.2 @xmachines/json-render-core@^0.20.0-xm.2 @xmachines/json-render-xstate@^0.20.0-xm.2

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>

The composite provider. It wraps <ActorProvider> and JSONUIProvider in one component. Use it in most applications.

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 (the rendered content), fallback (the content while the actor view is null)

<PlayRenderer>

The leaf component without props. It reads the current spec and registry from the nearest <ActorProvider> or <PlayUIProvider> context, then renders them with <Renderer>. Put it inside one of those providers.

<PlayUIProvider :actor="actor" :registryResult="registryResult">
<PlayRenderer />
</PlayUIProvider>

<ActorProvider>

The low-level provider for a custom provider composition. It owns the complete actor lifecycle: the signal subscription, the state store of each view, the handler resolution, and the Vue context. Use <PlayUIProvider> when you do not need this control.

PropTypeRequiredDescription
actorAbstractActor & ViewableThe XMachines actor instance
registryResultDefineRegistryResultResult of defineRegistry()
storeStateStoreExternal controlled state store
onRenderErrorRenderErrorHandlerOverride render error handler

Functions

defineRegistry(catalog, options)

This function is the drop-in replacement for defineRegistry from @xmachines/json-render-vue. Always import it from @xmachines/play-vue when you work with a Vue SFC, not from @xmachines/json-render-vue. The wrapper finds each .vue SFC in the components map, and wraps it with h(). A Vue composable, and also a composable that uses inject, then works correctly inside <script setup>.

import { defineRegistry } from "@xmachines/play-vue";
// NOT: import { defineRegistry } from "@xmachines/json-render-vue"
import { myCatalog } from "./catalog.js"; // as in the Quick Start
import LoginSFC from "./views/Login.vue";
import DashboardSFC from "./views/Dashboard.vue";
const registryResult = defineRegistry(myCatalog, {
components: {
Login: LoginSFC, // .vue SFC — auto-wrapped via h()
Dashboard: DashboardSFC,
},
actions: {
login: async (args, setState, state) => {
/* ... */
},
},
});

A plain ComponentFn function also works, and the wrapper passes it through without a change. One registry can hold both SFCs and plain functions.

useActor()

The Vue composable that gives the raw actor inside a PlayRenderer tree. A deeply nested component then does not need the actor as a prop.

import { useActor } from "@xmachines/play-vue";
// Inside a component rendered by PlayRenderer:
const actor = useActor();
actor.send({ type: "SUBMIT" });

It throws when the caller is outside an <ActorProvider> or a <PlayUIProvider> tree.

usePlayView()

Access the current ViewContextValue{ spec, handlers, registry, store } — from inside an <ActorProvider> tree.

import { usePlayView } from "@xmachines/play-vue";
// Inside setup() of a component within an ActorProvider tree:
const view = usePlayView();
// view.spec, view.handlers, view.registry, view.store

Note: usePlayView was previously named getPlayViewContext. The old name is still exported as a deprecated alias and will be removed in the next major.


Re-exported from @xmachines/json-render-vue

This package re-exports the following, so that a consumer imports 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
pnpm --filter @xmachines/play-vue test
# Watch mode
pnpm --filter @xmachines/play-vue run test:watch
# With coverage (80% threshold enforced on lines, functions, branches, statements)
pnpm exec vitest run --coverage --config packages/play-vue/vitest.config.ts

The tests use Vitest in a jsdom environment. They mount the components with @vue/test-utils.


License

MIT — see LICENSE.

@xmachines/play-vue - Vue 3 renderer for the XMachines Play architecture

This package is a thin Vue rendering layer. It observes the actor signals and renders the UI components with @xmachines/json-render-vue. Vue reactivity only triggers the re-render. The signals are the source of truth.

The package re-exports defineRegistry, which knows an SFC and wraps each .vue SFC with h(). It also re-exports useBoundProp, ComponentFn, ComponentContext, and every json-render provider. A consumer therefore imports everything from @xmachines/play-vue, and not from @xmachines/json-render-vue.

Interfaces

Type Aliases

Variables

Functions

References

ActorProvider

Renames and re-exports PlayRenderer


PlayUIProvider

Renames and re-exports PlayRenderer