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:
@@ -307,20 +307,42 @@ In the template you don't mutate anything — you send messages:
|
||||
### 2d. Side effects (HTTP) without polluting the reducer
|
||||
|
||||
`reduce` is pure — it must not call the network. So how does a submit happen?
|
||||
The component has a small **command** method that does the impure work and then
|
||||
sends messages describing the outcome:
|
||||
`createStore` (`libs/shared/src/application/store.ts`) takes an optional **effect
|
||||
map**: one handler per state tag, registered next to the reducer, run when the
|
||||
store **enters** that tag:
|
||||
|
||||
```ts
|
||||
async runIfSubmitting() {
|
||||
if (this.state().tag !== 'Submitting') return;
|
||||
this.profile.beginHerregistratie(); // 1. optimistic (see below)
|
||||
const r = await submitHerregistratie(s.data); // 2. the actual call
|
||||
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(); // 1. optimistic (see below)
|
||||
const r = await this.draftSync.submit({ uren: s.data.uren, documents: s.data.documents }); // 2. the actual call
|
||||
if (r.ok) {
|
||||
store.dispatch({ tag: 'SubmitConfirmed' }); // 3. tell the reducer what happened
|
||||
this.profile.confirmHerregistratie();
|
||||
} else {
|
||||
store.dispatch({ tag: 'SubmitFailed', error: r.error });
|
||||
this.profile.rollbackHerregistratie();
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
So the split is: **reducer = "what the new state is", command = "go do the thing,
|
||||
Two properties follow from "run on entry", and they are the reason this replaced an
|
||||
earlier idiom where a component called a hand-written submit method by hand after
|
||||
every `dispatch`:
|
||||
|
||||
1. **Entering the state runs the effect.** A `dispatch` cannot silently skip it —
|
||||
there is no separate call to forget.
|
||||
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 in `Submitting` is a reducer no-op, so no second effect fires.
|
||||
|
||||
One message is exempt from this rule: `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 — see `store.ts` for the full contract.
|
||||
|
||||
So the split is: **reducer = "what the new state is", effect = "go do the thing,
|
||||
then tell the reducer what happened."**
|
||||
|
||||
### 2e. Optimistic update + rollback, and shared state across pages
|
||||
@@ -571,7 +593,7 @@ client.dashboardView() })`
|
||||
→ GET `/api/v1/dashboard-view` → `httpClientFetch` → proxy → backend → back through the
|
||||
`parseDashboardView(json): Result` trust boundary → `RemoteData<DashboardView>` → rendered.
|
||||
|
||||
**A write (change address):** `runIfSubmitting()` (§2d) → `createSubmitChangeRequest`
|
||||
**A write (change address):** the `Submitting` effect (§2d) → `createSubmitChangeRequest`
|
||||
([`submit-change-request.ts`](../../../apps/ssp/src/app/registratie/application/submit-change-request.ts))
|
||||
→ `runSubmit` — the one try/catch that mints the `Idempotency-Key` and maps RFC-7807
|
||||
ProblemDetails → string ([`submit.ts`](../../../libs/shared/src/application/submit.ts)) →
|
||||
|
||||
Reference in New Issue
Block a user