@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@0.21.0-xm.6 @xmachines/play @xmachines/play-atomThe json-render packages are exact peers: install the version that this release names, 0.21.0-xm.6.
@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 |
ViewMeta, typedSpec | The view that an author writes in meta.view, and its identity helper |
PlaySpec | The spec that an actor publishes, and that a renderer receives |
ViewOutlet, composeViews | The composition of a parent view and the views of its substates |
BaseActorProviderProps, BaseViewContextValue | The framework-free half of the props and of the context of a provider |
createViewSession | The resolution of the view store that every provider reads |
createViewStoreLifecycle | The lifecycle of the state store of one view |
createReportGuard | The report of a contained failure, with a reset that the host holds |
toAtomState, attachRenderErrorHandler | The guards that a provider puts around a registry |
createRenderErrorBinding | One registry copy for each registry, which calls the newest handler |
createActorProxy | The stand-in for the actor that a provider hands to its descendants |
bindStoreHandlers | The action handlers of a registry, against a state store |
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 ViewMeta 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:const meta = { view: typedSpec({ root: "root", elements: { root: { type: "Dashboard", props: { username: { $state: "/context/username" } }, children: [], }, }, }),};ViewMeta and PlaySpec
ViewMeta is the view that you write in meta.view: the Spec type of
@xmachines/json-render-core, and the outlets of a parent view. PlaySpec is the view that the
actor derives from it, and that a renderer receives: it carries viewKey, the /context
projection, and viewParts for a composed view, and no outlets.
PlaySpec 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 { ViewMeta } from "@xmachines/play-view";
const spec: ViewMeta = { root: "root", elements: { root: { type: "Profile", props: { username: { $state: "/context/username" } }, children: [], }, },};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.
outlets and composeViews(tree)
A meta.view that declares outlets shares the screen with the views of its active substates.
Each outlet is a ViewOutlet: the element of the parent view that receives the view of a
child, and an optional named slot of that element. Without slot, the child view goes into the
children of the element.
outlets: { menu: { element: "shell", slot: "sidebar" }, // the child state "menu" "*": { element: "shell" }, // every other active child}The view derivation of @xmachines/play-xstate resolves the active
views into a ViewTree, and composeViews grafts that tree into one flat spec. The keys of the
outer view stay as the author wrote them, and each key of a child view gets the id of its state
as a prefix ("app.page/root"). The function makes a new object on each call. A caller that
emits the composed view on each snapshot keeps one result for each tree, so that the emit gate
sees no change on a context assign: play-xstate keeps one for each tree of each machine. An outlet that names an absent element
throws InvalidViewOutletError from @xmachines/play-view/errors, and two views that put the
same key into the composition throw ViewElementKeyCollisionError.
A composed spec carries viewParts, the ids of the composed states, and its viewKey is the id
of the outermost view. createViewStoreLifecycle therefore keeps the store when a page changes,
and it writes the authored keys of the new page that the store does not hold yet. In the
controlled mode, the caller owns the seed, so the lifecycle writes no authored key. It refreshes
/context only.
The State Machines guide shows a complete machine.
createViewSession(actor, store, options)
The view session resolves the store of the view for a provider, and the five renderers share it.
It resolves when the actor emits, when the host hands over a new actor or controlled store
(setInputs), and for a retry (retry). It never resolves in a render.
A provider reads one immutable snapshot: view, the guarded store, the generation of the store,
and a failure. It binds subscribe and getSnapshot to the primitive of its framework. The first
listener connects the session, and the first read before it resolves without a write of a controlled
store, so a render may call getSnapshot. A new snapshot is a new attempt, so a provider keys its
subtree on generation and resets its error boundary on a new snapshot.
A failure of the resolution reaches options.onFailure, outside a render. A provider whose error
boundary reports a failure leaves that option out, and it throws snapshot.failure.error under the
boundary.
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";
declare const spec: PlaySpec; // the view that the double shows
// 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