Skip to content

@xmachines/play-react

API / @xmachines/play-react

React renderer for XMachines Play architecture with atom-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 @xmachines/core @xmachines/play @xmachines/play-atom @xstate/store @xmachines/json-render-react@0.21.0-xm.6 @xmachines/json-render-core@0.21.0-xm.6 @xmachines/json-render-xstate@0.21.0-xm.6

The json-render packages are exact peers: install the version that this release names, 0.21.0-xm.6.

Supported versions:

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

Usage

Standard usage — PlayUIProvider + PlayRenderer

The recommended pattern for actor-driven React rendering:

import { PlayUIProvider, PlayRenderer, defineRegistry, schema } from "@xmachines/play-react";
import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";
import { withView } from "@xmachines/play-xstate/view";
import { defineCatalog } from "@xmachines/json-render-core";
import type { AnyStateMachine } from "xstate";
import { z } from "zod";
declare const myMachine: AnyStateMachine; // your xstate machine (states carry meta.view specs)
// 1. Define the catalog: the components and the actions that a view can name
const myCatalog = defineCatalog(schema, {
components: {
Login: { props: z.object({ title: z.string() }) },
Dashboard: { props: z.object({ username: z.string() }) },
},
actions: {
login: { params: z.object({ username: z.string() }) },
logout: {},
},
});
// 2. Create and start the actor — a renderer needs the view capability
const actor = definePlayer({ machine: myMachine, actor: compose(PlayerActor, withView) })();
actor.start();
// 3. Define the component registry with action handlers
const registryResult = defineRegistry(myCatalog, {
components: {
Login: ({ props }) => <h1>{props.title}</h1>,
Dashboard: ({ props }) => <p>Welcome, {props.username}</p>,
},
actions: {
login: async (params) => {
if (params) actor.send({ type: "auth.login", username: params.username });
},
logout: async () => actor.send({ type: "auth.logout" }),
},
});
// 4. Render — atoms 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:

import { PlayUIProvider, PlayRenderer, type PlayUIProviderProps } from "@xmachines/play-react";
// actor, registryResult from the Quick Start above
declare const actor: PlayUIProviderProps["actor"];
declare const registryResult: PlayUIProviderProps["registryResult"];
declare const myRouter: { push(path: string): void }; // your router
<PlayUIProvider
actor={actor}
registryResult={registryResult}
navigate={(path) => myRouter.push(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";
import type { ActorProviderProps } 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
declare const actor: ActorProviderProps["actor"];
declare const registryResult: ActorProviderProps["registryResult"];
<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 atoms directly

import { useState } from "react";
import { useAtomEffect, type PlayUIProviderProps } from "@xmachines/play-react";
function MyComponent({ actor }: { actor: PlayUIProviderProps["actor"] }) {
const [view, setView] = useState(() => actor.currentView.get());
// The first argument names every atom that runs the effect.
useAtomEffect(
[actor.currentView],
() => {
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 atom 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
useAtomEffect(sources, callback, deps?)Subscribes to the atoms of sources. It runs the callback again when one of them changes, and the callback triggers the re-render with its own setState. A read inside the callback tracks nothing, so name every dependency in sources. 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>
PlayErrorBoundaryPropsProps for <PlayErrorBoundary>
PlayErrorBoundaryStateState shape for <PlayErrorBoundary>
AnyPlayActorType alias for PlayActor — the bare actor type that the context providers use
ViewContextValueThe value shape that usePlayView() returns
DefineRegistryResultThe result of defineRegistry, for a registry that you declare or pass on
OnRenderErrorThe (error, name, info) handler of onRenderError
RenderErrorInfoThe info of a report: kind, key, type, and action
RenderErrorKindThe kind of a failure, such as "element", "action", or "root"
RenderErrorHandlerThe older (error, name) handler type. It stays for compatibility

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 atoms (@xmachines/play-atom) are the source of truth. PlayUIProvider observes the actor atoms with useAtomEffect, and it renders again when the current view changes. It groups rapid atom updates into microtasks, so React does not render more often than necessary.

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 atoms 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 atom changes.

Key principle: React state is NEVER the place for the business logic. It only triggers the render cycle of React. The atoms 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