Skip to content

Composition

Stator composes at two levels: machines compose into a state graph, and components compose into a UI tree. They’re separate mechanisms with separate jobs.

Machines relate to each other in exactly two ways — one synchronous, one reactive.

reads: is a synchronous pull. A machine declares the machines it depends on, and inside its transitions it can read their current state:

reads: [ProductsMachine],
// inside a transition:
do: (ctx, ev, { reads }) => {
const product = reads.ProductsMachine.byId(ev.productId)
if (product) ctx.items.push({ productId: ev.productId, unitPrice: product.price, quantity: 1 })
},

reads.ProductsMachine is typed from the declared tuple, with the read machine’s selectors preserved. Reads resolve server-side — which is why a machine that reads another is server-pinned.

emits lets a machine announce a fact; subscribes lets another machine react to it. The emitting machine names domain facts and attaches a payload selector:

emits: { ITEM_ADDED: { payload: cartSnapshot } },
subscribes: [
{ from: CheckoutMachine, event: 'ORDER_PLACED', dispatch: 'CLEAR' },
],

Where reads is “I need your state right now,” subscribes/emits is “tell me when this happened.” The payload selector runs after the emitting transition commits, so subscribers see post-transition state.

A machine’s lifecycle shapes how it composes:

  • session→app delivery works (an admin/app machine can subscribe to a session machine’s emits; the runtime injects the originating sourceSessionId).
  • app→app is wired at app boot.

In a template, a capitalized tag invokes a Stator component; a lowercase tag is a plain HTML element:

<CartPage cart={cart} products={products} /> <!-- component -->
<section class="cart"></section> <!-- HTML element -->

Components receive machines and data as props via Stator.props<...>(), and the children render eagerly at the call site.

A component declares slots for content with <children>, and callers target them with child="...":

<!-- in BaseLayout.stator -->
<header><children name="header" /></header>
<main><children /></main>
<!-- a caller -->
<BaseLayout>
<nav child="header"></nav> <!-- fills the "header" slot -->
<p>Goes to the default slot.</p>
</BaseLayout>

Note this is <children> + child="...", not the Web Components <slot> element — it’s a compile-time composition feature, resolved in a single eager render pass, with no shadow DOM involved. The compiler validates that a child="x" marker matches a declared <children name="x" />.

A route page is the same composition model with a frontmatter that uses Stator.reads([...]) instead of Stator.props. It pulls the machines it needs and composes layout and page components in its body — see Routing. The page is a component; “route” just means “discovered under routes/ and given request access.”