@xmachines/play-view
API / @xmachines/play-view
The shared view half of every renderer of the XMachines Play Architecture.
Installation
pnpm add @xmachines/play-viewPeer dependencies. Install them with the package:
pnpm add @xmachines/core @xmachines/json-render-core @xmachines/play @xmachines/play-atom@xmachines/core— the base package. This package reads the helpers of itsutilssubpath.@xmachines/play— the core protocol. This package reads the error classes from itserrorssubpath.@xmachines/play-atom— the atom primitives. The view store holds the spec in aReadonlyAtom.@xmachines/json-render-core— the spec type thatPlaySpecextends.
A renderer of a framework depends on this package already. Install it directly when you write a renderer of your own.
Overview
A renderer of a framework is two things: the view lifecycle, and the code that binds that lifecycle to the effects of the framework. This package holds the first one. @xmachines/play-react, @xmachines/play-vue, @xmachines/play-solid, @xmachines/play-svelte and @xmachines/play-dom hold the second one.
It also holds Viewable and PlaySpec. Every type here names a type of @xmachines/json-render-core, and that is the reason they are here and not in @xmachines/play-actor: an application that routes and renders no view installs this package never, and json-render never.
| Export | Description |
|---|---|
Viewable | The capability of an actor that publishes a currentView atom |
PlaySpec, typedSpec | The spec that an actor publishes, and its identity helper |
BaseActorProviderProps, BaseViewContextValue | The framework-free half of the props and of the context of a provider |
createViewStoreLifecycle | The lifecycle of the state store of one view |
createFailureLatch, createReportGuard | The report of a render failure, one time for each view |
toAtomState, attachRenderErrorHandler | The guards that a provider puts around a registry |
composePlayState, refreshContextSubtree, … | The projection of the machine context into the store |
API Summary
typedSpec(spec)
This identity helper gives a view-spec literal the type PlaySpec at the definition site. The
XState meta field has the type Record<string, unknown>. Therefore this helper is the place
where the spec shape receives the compile-time check and the IDE autocomplete. The helper has no
cost at run time.
import { typedSpec } from "@xmachines/play-view";
// In an XState machine meta block:meta: { view: typedSpec({ root: "root", elements: { root: { type: "Dashboard", props: { username: { $state: "/context/username" } }, children: [], }, }, }),}PlaySpec
This type extends the Spec type of @xmachines/json-render-core. Each derived view receives
the complete machine context in its state store, under the read-only /context subtree. A
spec therefore reads the context through the ordinary { $state: "/context/…" } grammar: in a
prop, in a visible condition, and in repeat.statePath.
/context is read-only by design. Nothing can write to it. The machine context changes through
an event only. A $bindState write or a setState write under /context throws an error, and
the error names the event to send. This is the model: the bindable ephemeral state is at the
root of the store, from spec.state; the domain state is in the machine, and it changes through
events that are meaningful and easy to inspect.
The path shows the origin of each value. /context/params/username comes from the URL.
/context/username belongs to the machine. One value can never hide the other.
The model projects everything, and this has two consequences. The first consequence is exposure. The complete context is visible to the client in the view store, which includes a debug panel, an inspector, and a validator. The context is a client-side value in each case, so keep a secret out of it.
The second consequence is emission granularity. The emit gate compares the context field by
field, at the top level only. Therefore an event that changes any field emits the view again
with the same viewKey. A provider refreshes /context in the live store, and it does not seed
the store again. The component does not remount, and the ephemeral view state and the focus
stay. However, a new emission is still a render pass in the framework layer. Keep
high-frequency ephemeral data, such as a draft for each keystroke or a timer, in the view store
(spec.state with $bindState) or in a child actor. The domain state belongs in the context. A
keystroke does not.
import type { PlaySpec } from "@xmachines/play-view";
const spec: PlaySpec = { root: "root", elements: { root: { type: "Profile", props: { username: { $state: "/context/username" } }, children: [], }, },};Historical note: an earlier version had a
contextPropsfield. At first the field drove an implicit prop-enrichment pass. That pass merged the allowlisted context fields and the URL params into the props of every element. We removed it, because it put values into components that never asked for them, and it let URL data from the user hide machine-owned state. The field was then a projection filter for a short time. We removed that filter too, because a limit on what a view can read added machinery without a real problem to solve. Always validate the derived view (actor.currentView.get()), not the rawmeta.view. Thestateof the derived spec carries the projection, so a tool such asvalidateSpecsees a spec that is consistent with itself.
Viewable
Interface for actors that expose a renderable view atom.
currentView is a ReadonlyAtom, because an actor DERIVES the view from its state. Only
the actor decides what the view is, and that rule is in the type rather than in prose. A
writable Atom still satisfies the interface structurally, so a test double drives a
renderer by holding its own writable reference:
import type { Viewable } from "@xmachines/play-view";import type { PlaySpec } from "@xmachines/play-view";import { createAtom } from "@xmachines/play-atom";
// The real actor derives this atom. A double writes its own.const atom = createAtom<PlaySpec | null>(null);const viewable: Viewable = { currentView: atom };
atom.set(spec); // the double drives the renderer through ITS referenceBaseActorProviderProps<TRegistry>
The framework-agnostic base props. Every ActorProvider implementation shares them: React, Vue, Solid, and Svelte. Each framework renderer package extends this interface.
import type { BaseActorProviderProps } from "@xmachines/play-view";import type { DefineRegistryResult } from "@xmachines/json-render-react";
interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> { fallback?: React.ReactNode; children: React.ReactNode;}BaseViewContextValue<TRegistry>
The framework-agnostic base of the ViewContextValue type in each framework. It holds the spec, handlers, registry, and store fields. These fields are identical in React, Vue, Solid, and Svelte.
Documentation
License
MIT