@xmachines/play-vue
API / @xmachines/play-vue
Vue 3 renderer for the XMachines Play Architecture. It observes the actor atoms and renders the UI through
@xmachines/json-render-vue.
Overview
@xmachines/play-vue is the Vue 3 rendering layer of XMachines Play. It connects the atoms (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 atoms. They never decide a state transition.
- Atom-Only Reactivity — the atoms 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
pnpm add @xmachines/play-vuePeer dependencies. Install them with the package:
pnpm add vue@^3.5.0 @xmachines/core @xmachines/play @xmachines/play-atom @xstate/store@^4.2.3 @xmachines/json-render-vue@^0.21.0-xm.1 @xmachines/json-render-core@^0.21.0-xm.1 @xmachines/json-render-xstate@^0.21.0-xm.1@xstate/store is the peer of @xmachines/play-atom, and not of this
package: this package reads the atom through @xmachines/play-atom and
imports the engine in no source file. Install ONE copy of it.
Quick Start
<template> <PlayUIProvider :actor="actor" :registryResult="registryResult"> <PlayRenderer /> </PlayUIProvider></template>
<script setup lang="ts">import { defineRegistry, PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";import { withView } from "@xmachines/play-xstate/view";import { myMachine } from "./machine.js";import { myCatalog } from "./catalog.js";import HomeSFC from "./views/Home.vue";import LoginSFC from "./views/Login.vue";
// A renderer needs the view capabilityconst createPlayer = definePlayer({ machine: myMachine, actor: compose(PlayerActor, withView) });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.
| Prop | Type | Required | Description |
|---|---|---|---|
actor | PlayActor & Viewable | ✅ | The XMachines actor instance |
registryResult | DefineRegistryResult | ✅ | Result of defineRegistry() |
store | StateStore | — | External controlled state store (optional) |
onRenderError | RenderErrorHandler | — | Error handler for render failures |
onError | (error: unknown, reset: () => void) => void | — | Error handler of the outer boundary; the second parameter is the reset |
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 (the rendered content), fallback (the content while the actor view is null, and while the boundary holds an error)
The provider contains an error of the render of a descendant — the render function, the setup function, and a lifecycle hook — so that a subtree that throws cannot take the whole application down. It calls onError, and it renders the fallback slot instead of the children. The next view emission clears the error, so a view transition retries the render. A throw of a catalog component reaches the per-element boundary of @xmachines/json-render-vue first, and onRenderError reports it; this boundary is the outer net for everything else.
The boundary stops a render error, so app.config.errorHandler sees it no longer. Without an onError prop the provider therefore writes the error to console.error.
The second parameter is the reset, for a retry that the host starts. It renders the view that the actor holds at the moment of the call, so a retry cannot rewind the screen to the view that failed. A reset that the host calls from inside the handler does nothing, because no input changed between the two attempts, and a reset after the provider goes away does nothing. The five renderers hold the same three rules.
A handler that throws reaches no caller. The provider contains that throw, and it writes the throw to console.error. The errored flag schedules a render, so a throw that left the handler would stop that render and skip the return false of onErrorCaptured: the subtree that threw stays on the screen and the error reaches app.config.errorHandler as well. Report a failure from onError. To escalate one, raise it from a task of your own — rethrowLater(error) of @xmachines/core/utils — which reaches the global handler of the page and leaves the containment whole. Install that package next to this one, because your code imports it and a package manager such as pnpm does not let your code import a package that it does not declare. The five renderers hold this one rule.
onErrorCaptured of Vue also fires for an error of an event handler and of a watcher callback, which the boundaries of React, Solid, and Svelte each see never. The provider leaves such an error alone: the view stays on the screen, onError stays silent, and the error continues to app.config.errorHandler exactly as it does without this provider. A click handler that throws therefore does not replace your view with the fallback, and it reaches your global handler one time. onError reports a failure of the render, in every renderer.
Vue reports such an error a second time where the application wires no app.config.errorHandler: it re-throws the error, and a queued job carries it back as a failure of a render. The provider recognises that repeat and holds the view. It writes the error value to console.error with the name of this package, because Vue writes that value nowhere on this path, and onError stays silent. A development build re-throws every time, and a production build only where the application sets app.config.throwUnhandledErrorInProduction.
One case escapes that rule. An ancestor errorCaptured hook that returns false stops Vue before the re-throw, so the record of the provider waits for a repeat that never comes. A second failure in the same flush can take that record. It must carry the same error value, and it must unwind out of the update job of a parent. Vue reports such a failure as a component update. The provider then holds the stale subtree on the screen, and onError stays silent.
<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 atom subscription, the state store of each view, the handler resolution, and the Vue context. Use <PlayUIProvider> when you do not need this control.
| Prop | Type | Required | Description |
|---|---|---|---|
actor | PlayActor & 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)
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 Startimport 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.storeRead a field where you use it, and destructure it not. The value is a live view of the provider, so
const { spec } = usePlayView()freezes at the first read and the screen stops following the actor — with no error and no warning. Writeview.specat the place that needs it.
usePlayView() returns a Proxy: every property read goes to the live context of the provider.
Note:
usePlayViewwas namedgetPlayViewContextbefore. That alias is gone.
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
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 atoms and renders the UI components with @xmachines/json-render-vue. Vue reactivity only triggers the re-render. The atoms 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
- ActionProviderProps
- ActorProviderProps
- ComponentContext
- DefineRegistryResult
- JSONUIProviderProps
- PlayUIProviderProps
- RendererProps
- StateProviderProps
- ValidationProviderProps
- ViewContextValue
Type Aliases
Variables
- ActionProvider
- JSONUIProvider
- PlayRenderer
- Renderer
- schema
- StateProvider
- ValidationProvider
- VisibilityProvider
Functions
References
ActorProvider
Renames and re-exports PlayRenderer
PlayUIProvider
Renames and re-exports PlayRenderer