import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Foundations/State Machines (TEA)" />

# 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<WizardState, WizardMsg>) {
  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<WizardState, WizardMsg>(initial, reduce);
readonly model = this.store.model; // Signal<WizardState> — 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 `<app-async>` to render
(`BriefStore.remoteData` is the worked example) rather than teaching every consumer to
hand-roll a `@switch` over the machine's own tags.
