Skip to content

@xmachines/play-react

API / @xmachines/play-react

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

License: MIT Version

Installation

Terminal window
pnpm add @xmachines/play-react

Peer dependencies. Install them separately:

Terminal window
pnpm add react react-dom xstate @xstate/store @xmachines/json-render-react @xmachines/json-render-core @xmachines/json-render-xstate

Supported versions:

  • react / react-dom: ^18.0.0 || ^19.0.0
  • xstate: ^5.31.0
  • @xstate/store: ^3.17.0
  • @xmachines/json-render-*: ^0.20.0-xm.2

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";
import { myMachine } from "./machine.js"; // your xstate machine (states carry meta.view specs)
import { myCatalog } from "./catalog.js"; // defineCatalog(schema, ...) result, using the schema from "@xmachines/json-render-react/schema"
import { Login, Dashboard } from "./components.js"; // your React components
// 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:

// actor, registryResult from the Quick Start above
<PlayUIProvider
actor={actor}
registryResult={registryResult}
navigate={(path) => history.pushState(null, "", path)}
validationFunctions={{ isEmail: (v) => /^.+@.+$/.test(String(v)) }}
>
<PlayRenderer />
</PlayUIProvider>

Custom provider composition

Use ActorProvider directly when you need to compose providers manually:

import type { ReactNode } from "react";
import { ActorProvider, JSONUIProvider, PlayRenderer, usePlayView } from "@xmachines/play-react";
// Handlers and store live in ViewContext, so an inner bridge component must
// read them via usePlayView() and forward all three to JSONUIProvider —
// passing only `registry` would drop the action handlers and create a fresh store.
function Bridge({ children }: { children: ReactNode }) {
const view = usePlayView();
return (
<JSONUIProvider registry={view.registry} handlers={view.handlers} store={view.store}>
{children}
</JSONUIProvider>
);
}
// actor, registryResult from the Quick Start above
<ActorProvider actor={actor} registryResult={registryResult}>
<Bridge>
<PlayRenderer />
</Bridge>
</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 { useState } from "react";
import { useSignalEffect } from "@xmachines/play-react";
function MyComponent({ actor }) {
const [view, setView] = useState(null);
useSignalEffect(() => {
setView(actor.currentView.get());
}, [actor]); // deps: re-subscribe when the actor prop swaps
return <div>{view?.root}</div>;
}

API Summary

Components

ExportDescription
<PlayUIProvider>Composite provider. It wraps ActorProvider and JSONUIProvider. Use it as the 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>The low-level provider. It owns the actor bridge, the signal subscription, and the StateStore lifecycle of each view.
<PlayErrorBoundary>The React class error boundary that catches a render error of a catalog component.

Hooks

ExportDescription
useSignalEffect(callback, deps?)Subscribes to the TC39 signal changes. It runs the callback again when a signal that the callback reads changes, and the callback triggers the re-render with its own setState. The optional deps array creates the subscription again, like useEffect. The hook removes the subscription 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 that the context providers use
ViewContextValueThe value shape that usePlayView() returns
RenderErrorHandlerError handler callback type for render errors

Re-exports from @xmachines/json-render-react

@xmachines/play-react re-exports the complete @xmachines/json-render-react surface, so a consumer needs one import only:

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

Key Principle

React state is never the place for the business logic. It only triggers the render cycle of React. The signals (@xmachines/play-signals) are the source of truth. PlayUIProvider observes the actor signals with useSignalEffect, and it renders again when the current view changes. It groups rapid signal updates into microtasks, so React does not render more often than necessary.

Testing

Run unit tests (jsdom environment):

Terminal window
pnpm --filter @xmachines/play-react test

Run tests with coverage:

Terminal window
pnpm --filter @xmachines/play-react run test:coverage

Run the browser integration tests. They require Chromium:

Terminal window
pnpm --filter @xmachines/play-react run test:browser

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

License

MIT — see LICENSE.

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

This package is the React rendering layer, and it works through providers. It observes the actor signals and renders the UI components with @xmachines/json-render-react. The architecture can therefore change the framework, because React is only a render target that subscribes to the signal changes.

Key principle: React state is NEVER the place for the business logic. It only triggers the render cycle of React. The signals are the source of truth.

Standard use:

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

The escape hatch (a custom composition): use <ActorProvider> directly, and give the ViewContext values (registry, handlers, store from usePlayView()) to <JSONUIProvider> through an inner bridge component. If you give only registry, you lose the action handlers and you make a new store. PlayUIProvider is this exact composition. Read its source for the reference bridge.

Classes

Interfaces

Type Aliases

Variables

Functions