Skip to content

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

License: MIT Version


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

Terminal window
pnpm add @xmachines/play-vue

Peer dependencies. Install them with the package:

Terminal window
pnpm add vue@^3.5.0 @xmachines/play @xmachines/play-atom @xstate/store@^4.2.3 @xmachines/json-render-vue@^0.20.0-xm.4 @xmachines/json-render-core@^0.20.0-xm.4 @xmachines/json-render-xstate@^0.20.0-xm.4

@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

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, 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 capability
const 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.

PropTypeRequiredDescription
actorPlayActor & Viewable✅The XMachines actor instance
registryResultDefineRegistryResult✅Result of defineRegistry()
storeStateStore—External controlled state store (optional)
onRenderErrorRenderErrorHandler—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
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, 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 — queueMicrotask(() => { throw error; }) — which reaches the global handler of the page and leaves the containment whole. 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.

PropTypeRequiredDescription
actorPlayActor & Viewable✅The XMachines actor instance
registryResultDefineRegistryResult✅Result of defineRegistry()
storeStateStore—External controlled state store
onRenderErrorRenderErrorHandler—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 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

Read 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. Write view.spec at the place that needs it.

usePlayView() returns a Proxy: every property read goes to the live context of the provider.

Note: usePlayView was named getPlayViewContext before. 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

Type Aliases

Variables

Functions

References

ActorProvider

Renames and re-exports PlayRenderer


PlayUIProvider

Renames and re-exports PlayRenderer