Skip to content

@xmachines/play-react

API / @xmachines/play-react

React renderer for XMachines Play architecture with signal-driven rendering.

Part of the xmachines-js monorepo.

Installation

Terminal window
npm install @xmachines/play-react

Peer dependencies (must be installed separately):

Terminal window
npm install react react-dom xstate @xstate/store @json-render/react @json-render/core @json-render/xstate

Supported versions:

  • react / react-dom: ^18.0.0 || ^19.0.0
  • xstate: ^5.31.0
  • @xstate/store: ^3.17.0
  • @json-render/*: ^0.18.0

Usage

Standard usage — PlayUIProvider + PlayRenderer

The recommended pattern for actor-driven React rendering:

import { PlayUIProvider, PlayRenderer, defineRegistry } from "@xmachines/play-react";
import { definePlayer } from "@xmachines/play-xstate";
// 1. Create and start the actor
const actor = definePlayer({ machine: myMachine })();
actor.start();
// 2. Define the component registry with action handlers
const registryResult = defineRegistry(myCatalog, {
components: { Login, Dashboard },
actions: {
login: async ({ username }) => actor.send({ type: "auth.login", username }),
logout: async () => actor.send({ type: "auth.logout" }),
},
});
// 3. Render — signals drive view transitions automatically
function App() {
return (
<PlayUIProvider actor={actor} registryResult={registryResult}>
<PlayRenderer />
</PlayUIProvider>
);
}

With optional JSONUIProvider props

Pass navigation and validation helpers through PlayUIProvider:

<PlayUIProvider
actor={actor}
registryResult={registryResult}
navigate={(path) => router.push(path)}
validationFunctions={{ isEmail: (v) => /^.+@.+$/.test(String(v)) }}
>
<PlayRenderer />
</PlayUIProvider>

Escape hatch — custom provider composition

Use ActorProvider directly when you need to compose providers manually:

import { ActorProvider, JSONUIProvider, PlayRenderer } from "@xmachines/play-react";
<ActorProvider actor={actor} registryResult={registryResult}>
<JSONUIProvider registry={registryResult.registry}>
<PlayRenderer />
</JSONUIProvider>
</ActorProvider>;

Accessing the actor from inside the tree

import { useActor } from "@xmachines/play-react";
function SubmitButton() {
const actor = useActor();
return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
}

Subscribing to signals directly

import { useSignalEffect } from "@xmachines/play-react";
function MyComponent({ actor }) {
const [view, setView] = useState(null);
useSignalEffect(() => {
setView(actor.currentView.get());
});
return <div>{view?.component}</div>;
}

API Summary

Components

ExportDescription
<PlayUIProvider>Batteries-included provider: wraps ActorProvider + JSONUIProvider. Standard entry point.
<PlayRenderer>Zero-prop leaf component. Reads the current actor view from context and renders it. Must be inside PlayUIProvider or ActorProvider.
<ActorProvider>Escape-hatch primitive. Owns actor bridging, signal subscription, and per-view StateStore lifecycle.
<PlayErrorBoundary>React class error boundary for catching catalog component render errors.

Hooks

ExportDescription
useSignalEffect(callback)Subscribes to TC39 signal changes; re-runs the callback and forces a React re-render when any accessed signal changes. Cleanup is automatic on unmount.
useActor()Returns the raw actor instance. Must be called inside an ActorProvider/PlayUIProvider tree.
usePlayView()Returns { spec, handlers, registry, store } for the current view. Must be called inside an ActorProvider/PlayUIProvider tree.

Types

ExportDescription
PlayUIProviderPropsProps for <PlayUIProvider>
ActorProviderPropsProps for <ActorProvider> (also exported as PlayRendererProps for migration compatibility)
PlayErrorBoundaryPropsProps for <PlayErrorBoundary>
PlayErrorBoundaryStateState shape for <PlayErrorBoundary>
AnyPlayActorType alias for AbstractActor<AnyActorLogic> — the bare actor type used by context providers
ViewContextValueValue shape returned by usePlayView()
RenderErrorHandlerError handler callback type for render errors

Re-exports from @json-render/react

@xmachines/play-react re-exports the full @json-render/react surface so consumers only need one import:

import {
defineRegistry,
useBoundProp,
JSONUIProvider,
StateProvider,
ActionProvider,
VisibilityProvider,
ValidationProvider,
Renderer,
} from "@xmachines/play-react";

Key Principle

React state is never used for business logic — only for triggering React’s render cycle. Signals (@xmachines/play-signals) are the source of truth. PlayUIProvider passively observes actor signals via useSignalEffect and re-renders when the current view changes. Rapid signal updates are batched via microtasks to prevent unnecessary React renders.

Testing

Run unit tests (jsdom environment):

Terminal window
npm test -w @xmachines/play-react

Run tests with coverage:

Terminal window
npm run test:coverage -w @xmachines/play-react

Run browser integration tests (requires Chromium):

Terminal window
npm run test:browser -w @xmachines/play-react

Coverage thresholds: 80% lines, functions, branches, and statements.

License

MIT — see LICENSE.

@xmachines/play-react - React renderer for XMachines Play architecture

Provides a provider-based React rendering layer that passively observes actor signals and renders UI components via @json-render/react. This package enables framework-swappable architecture where React is just a rendering target that subscribes to signal changes.

Key principle: React state is NEVER used for business logic—only for triggering React’s render cycle. Signals are the source of truth.

Standard usage:

<PlayUIProvider actor={actor} registryResult={registryResult}>
<PlayRenderer />
</PlayUIProvider>

Escape hatch (custom composition):

<ActorProvider actor={actor} registryResult={registryResult}>
<JSONUIProvider registry={registryResult.registry}>
<PlayRenderer />
</JSONUIProvider>
</ActorProvider>

Classes

Interfaces

Type Aliases

Variables

Functions

References

PlayRendererProps

Renames and re-exports ActorProviderProps


RenderErrorHandler

Re-exports RenderErrorHandler