import { Meta } from '@storybook/addon-docs/blocks'; # State machines (The Elm Architecture, in Angular) Every form or wizard with validation or submission in this app is wired the **same way**: one Model, one Msg union, one pure `reduce`, one command per side effect. Pick any one — `herregistratie.machine.ts` is the fullest worked example — and the shape transfers everywhere else. ## Model / Msg / reduce ```ts // Model — everything the UI needs to render, as ONE tagged union export type WizardState = { tag: 'step1'; draft: Draft } | { tag: 'step2'; valid: Valid } | …; // Msg — every way the Model is allowed to change export type WizardMsg = { tag: 'FieldChanged'; field: string; value: string } | { tag: 'NextStep' } | …; // reduce — PURE: (current, message) -> next. No I/O, no Date.now(), no randomness. export function reduce(s: WizardState, m: WizardMsg): WizardState { … } ``` Because the whole state is one value, a bug reproduces from a message log; because `reduce` is pure, every transition is a one-line assertion in a spec — no `TestBed`, no mocked HTTP, just `expect(reduce(state, msg)).toEqual(next)`. ## Commands: side effects stay OUT of the reducer `reduce` only ever answers "what is the new state" — it never calls `fetch`. A **command** (an `application/submit-*.ts` file, or a store method) does the I/O, then dispatches a message describing the outcome: ```ts // command = "go do it, then say what happened" — reduce never sees the HTTP call itself async function submit(store: Store) { const r = await adapter.submit(toDto(store.model())); store.dispatch( r.ok ? { tag: 'SubmitConfirmed', referentie: r.value } : { tag: 'SubmitFailed', error: r.error }, ); } ``` This is also how a machine receives **server-owned config** without becoming aware of HTTP: `intake.machine.ts`'s scholing threshold has an offline fallback (`SCHOLING_THRESHOLD_DEFAULT`) baked into the model, and a plain `SetPolicy` message that overwrites it once the real value arrives — the machine doesn't know or care that the value came from a `resource()` fetch. ## `createStore`: the one wiring idiom ```ts private store = createStore(initial, reduce); readonly model = this.store.model; // Signal — template reads this dispatch = this.store.dispatch; // template calls this, on click/input/etc — never mutates ``` A page or component **never** hand-rolls `signal(initialModel)` plus its own local `dispatch` function that calls `reduce` inline — that's the same idea reinvented with a worse name, and it's the thing a newcomer copies if two idioms are visible side by side. Wire every machine through `createStore`, full stop. `dispatch` uses `model.update(…)`, not `model.set(reduce(model(), msg))` — the latter reads `model()` _inside_ the call, which means an `effect()` that both reads `model` and calls `dispatch` would subscribe to its own write and livelock. `.update()`'s callback receives the current value directly, untracked. ## Naming - A top-level machine's types are **context-prefixed**: `ChangeRequestState`, `ChangeRequestMsg`, `WizardState`, `WizardMsg` — never bare `State`/`Msg`. A bare name reads fine in the one file that defines it and then collides (or forces an import alias) the moment two machines are open side by side. - A top-level machine exports `initial` (the starting Model) and `reduce` — unprefixed, since the file/module already disambiguates them at the import site (`import { initial, reduce } from './herregistratie.machine'`). - A **composable sub-machine** — one embedded _inside_ a parent Model, like `upload.machine.ts`'s upload-widget state living inside the registratie wizard's own Model — keeps **prefixed value exports** instead: `initialUpload`, `reduceUpload`. The parent machine already imports several machines' `initial`/`reduce`; prefixing the sub-machine's exports avoids a wall of `as` import aliases at the composition site. ## Derive, don't store If a value can be computed from the Model, it is **not** a field on the Model. The wizard's visible steps are `visibleSteps(answers)`, a pure function of the current answers — not a `visibleSteps: Step[]` field someone has to remember to keep in sync every time an answer changes. The reflex: before adding a field, ask "could this just be a function of what I already have?" ## Where RemoteData fits in A machine owns the **domain** lifecycle of what it holds once it exists (draft → submitted → approved, in the brief's case). It should generally _not_ also own the **fetch** lifecycle (loading/failed) for the initial GET that produces it — that's a generic concern `RemoteData` already models once, consistently, across the app (see [Foundations/RemoteData & Async](?path=/docs/foundations-remotedata-async--docs)). Where a machine's own state happens to have `loading`/`failed` tags that purely mirror that fetch, project them onto a `RemoteData` at the store layer for `` to render (`BriefStore.remoteData` is the worked example) rather than teaching every consumer to hand-roll a `@switch` over the machine's own tags.