docs: teach the effect map, not the deleted submit method (RD-09)
RD-05 through RD-08 replaced the hand-called submit method with createStore's effect map. Two teaching documents still showed the old method in a code block, as the answer to "how does a submit happen?". Both blocks also called a function that no longer exists. Rewrite the code block in ARCHITECTURE.md section 2d and its fp-tea-atomic-design.md counterpart. Both now show the effect map, keyed on the Submitting tag, using the same herregistratie worked example with its optimistic begin/confirm/rollback calls. Both use draftSync.submit, the call the two herregistratie wizards make today. State the two properties the old idiom lacked, since they are the reason for the change: entering a state runs its effect, so a dispatch cannot skip it; and double-submit protection is structural, because the effect fires only on a tag transition. Add one sentence on the Seed exemption: a mount or restore message must not trigger a submit. Fix the one runIfSubmitting() hop in the write walkthrough at ARCHITECTURE.md's line 574. The rest of section 6a stays stale on purpose — RD-31 owns it, including its line citations and dead paths. fp-tea-atomic-design.md's broken pre-monorepo paths stay stale too — RD-32 owns those. Set RD-09's Status to done and its README row to done in the same commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -332,35 +332,43 @@ sends messages on events — it never mutates:
|
||||
That is the loop: `state → template → event → dispatch(Msg) → reduce → new state →
|
||||
template`.
|
||||
|
||||
### 4d. Effects → a command that dispatches the outcome
|
||||
### 4d. Effects → the store's effect map dispatches the outcome
|
||||
|
||||
`reduce` is pure, so it can't call the network. The component holds a small **command**
|
||||
method. It does the impure work, then dispatches a `Msg` describing what happened — the
|
||||
result re-enters through the same pure loop:
|
||||
`reduce` is pure, so it can't call the network. `createStore`
|
||||
(`libs/shared/src/application/store.ts`) takes an optional **effect map**: one
|
||||
handler per state tag, run when the store **enters** that tag. The handler does the
|
||||
impure work, then dispatches a `Msg` describing what happened — the result re-enters
|
||||
through the same pure loop:
|
||||
|
||||
```ts
|
||||
private async runIfSubmitting() {
|
||||
const s = this.state();
|
||||
if (s.tag !== 'Submitting') return;
|
||||
this.profile.beginHerregistratie(); // optimistic flag (shared store)
|
||||
const r = await submitHerregistratie(s.data); // the actual I/O — a Result
|
||||
if (r.ok) { this.dispatch({ tag: 'SubmitConfirmed' }); this.profile.confirmHerregistratie(); }
|
||||
else { this.dispatch({ tag: 'SubmitFailed', error: r.error }); this.profile.rollbackHerregistratie(); }
|
||||
}
|
||||
private store = createStore<WizardState, WizardMsg>(initial, reduce, {
|
||||
Submitting: async (s, store) => {
|
||||
this.profile.beginHerregistratie(); // optimistic flag (shared store)
|
||||
const r = await this.draftSync.submit({ uren: s.data.uren, documents: s.data.documents }); // the actual I/O — a Result
|
||||
if (r.ok) {
|
||||
store.dispatch({ tag: 'SubmitConfirmed' });
|
||||
this.profile.confirmHerregistratie();
|
||||
} else {
|
||||
store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
this.profile.rollbackHerregistratie();
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The command itself (`src/app/herregistratie/application/submit-herregistratie.ts`)
|
||||
returns a `Result` — success-or-error as a value, never a thrown exception:
|
||||
Running the effect **on entry**, instead of a component calling a submit method by
|
||||
hand after every `dispatch`, gives two properties the hand-called version lacked:
|
||||
|
||||
```ts
|
||||
export async function submitHerregistratie(data: Valid): Promise<Result<string, void>> {
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
if (data.uren === 0) return err('Aanvraag afgewezen: geen gewerkte uren geregistreerd.');
|
||||
return ok(undefined);
|
||||
}
|
||||
```
|
||||
1. **Entering the state runs the effect.** A `dispatch` cannot silently skip it.
|
||||
2. **Double-submit protection is structural.** The effect fires only on a tag
|
||||
**transition** (`Editing → Submitting`). A second `Submit` message while the
|
||||
store is already `Submitting` is a reducer no-op, so no second effect fires.
|
||||
|
||||
The split, in one line: **reducer = "what the new state is"; command = "go do the
|
||||
One message is exempt: `Seed`, the mount/restore message every wizard sends on
|
||||
load. A `Seed` transition into `Submitting` (a resumed draft, a Storybook story)
|
||||
must not trigger a submit, so `createStore` skips the effect for it.
|
||||
|
||||
The split, in one line: **reducer = "what the new state is"; effect = "go do the
|
||||
thing, then say what happened."** Incoming effects (an arriving HTTP value, a
|
||||
server-owned config) are wired with `effect()` and `untracked()` so the dispatch
|
||||
doesn't loop on its own write — see the BRP prefill and policy-threshold effects in
|
||||
|
||||
Reference in New Issue
Block a user