Peer machines
When a component has two independent axes of state — say a popup that’s open/closed and a submenu that’s shown/hidden — each axis is its own machine. compose runs them as one unit (orthogonal regions, without nesting):
import { machine, compose } from '@dunky-dev/state-machine'
const popup = machine({ /* closed / open */})const submenu = machine({ /* none / shown */})
const combobox = compose({ popup, submenu })
combobox.start() // starts every membercombobox.stop() // stops all + disposes sync and combine
// members stay independent — drive and read each directlypopup.send({ type: 'focus' })submenu.send({ type: 'open' })sync — cross-region coordination
Section titled “sync — cross-region coordination”React when any member changes. Coarse — it wakes on any change to any member; read what you need inside:
combobox.sync(() => { if (popup.matches('closed')) submenu.send({ type: 'close' })})combine — unified selection
Section titled “combine — unified selection”Derive one value across members. Re-evaluates on any member change; fires only when the combined value changes:
const view = combobox.combine(() => ({ open: popup.matches('open'), sub: submenu.state,}))
view.value // { open: true, sub: 'shown' }view.subscribe(render)Both sync and combine are auto-disposed on combobox.stop().
Additive, not multiplicative
Section titled “Additive, not multiplicative”Composing avoids the state explosion of folding multiple axes into one machine:
// popup: 2 states, loader: 4 statesconst combobox = compose({ popup, loader })// → 2 + 4 = 6 nodes total, not 2 × 4 = 8vs. true parallel states
Section titled “vs. true parallel states”Members are independent peers — a send goes to one member, not broadcast. Cross-region behavior is expressed explicitly via sync. That’s the deliberate trade: simpler than nested/parallel statecharts, at the cost of a shared event bus.
See States for the full decision guide on when to use computed, compose, or tags.