Skip to content

Commit ff5bcd0

Browse files
committed
docs(adr-0014): amend for chain-based signature
The architectural decision (composition hook in a subpath, not extending CallContext) is unchanged. What this amendment captures: - Chosen-option bullet updated to the positional signature with `submit(payload).orEnd(value)` chain for the Fallback response. - Options-object signature (the previous iteration in this PR) moved to Considered options as an explicit rejected alternative, so a future reader doesn't re-propose it without knowing the trade-off. - Replaced the obsolete "TypeScript overloads on the options object" consequence with the new "positional + chain" mechanism, exporting both `Trigger<Payload>` and `ChainTrigger<Payload, Response>`. - New consequence: the type system no longer enforces the Fallback response (regression accepted from grilling Q2). - New consequence: Fallback response is per-Trigger-callsite, not per-Callable (Picker-style flexibility from grilling Q3). - New consequence: the Manual-close path is a supported branch (from grilling Q6) β€” `submit()` without a chain is a legitimate pattern.
1 parent 8169fc4 commit ff5bcd0

1 file changed

Lines changed: 6 additions & 2 deletions

File tree

β€Ždocs/adr/0014-mutation-flow-as-composition-hook.mdβ€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Issue #22 asks for a first-class answer to *click β†’ run async β†’ keep open on
55
## Considered options
66

77
- **Primitive on `CallContext` (PR #81 shape).** `call.pending` and `call.mutate(payload)` exposed by the lib itself, with `mutationFn` living in a new `CallOptions` second-arg of `call()` / `upsert()` and `MutationPayload` inserted as the 3rd generic of `createCallable` (RootProps moves to 4th). Rejected: requires ~+159 LOC in `createCallable`, mutates a public type (`CallContext`), bumps the brotli budget 1 KB β†’ 1.25 KB, and forces a BREAKING reorder of generics on consumers who already used `<Props, Response, RootProps>`. The functional win over the hook is marginal: HMR-preserved `pending` across saves and the option to wire `Callable.mutate(...)` from outside later β€” both rare. Not worth the lib-core surface area.
8-
- **Composition hook in a subpath (chosen).** `useMutationFlow(call, { mutationFn, fallback? })` lives in `react-call/mutation-flow`. Pending is local `useState` inside the user component. The handler receives a narrow `MutationCall<Response>` view of the call (just `{ end }`) and decides when to close. Two TypeScript overloads on the options object encode the invariant "fallback is required iff `mutationFn` is possibly-undefined", so the wart of a dead-weight optional field is closed at compile time. Lib core: zero changes.
8+
- **Composition hook in a subpath (chosen).** `useMutationFlow(call, mutationFn)` lives in `react-call/mutation-flow`. Pending is local `useState` inside the user component. The handler receives a narrow `MutationCall<Response>` view of the call (just `{ end }`) and decides when to close. The Fallback response, when needed, is delivered at the Trigger callsite as a method chain: `submit(payload).orEnd(value)`. Lib core: zero changes.
9+
- **Options-object signature with TS-enforced Fallback response.** `useMutationFlow(call, { mutationFn, fallback? })` with two overloads encoding the invariant "fallback is required iff `mutationFn` is possibly-undefined", so the consumer is forced to declare the fallback statically whenever the prop is typed nullable. Rejected during the PR #82 grilling: the fallback ends up declared in a place disconnected from where the Trigger fires, and the hook can only express one fallback per Callable even when distinct callsites (e.g. a Picker with `.orEnd('A')` / `.orEnd('B')`) naturally pair with distinct response values. Moving the fallback to the Trigger callsite via `.orEnd(value)` co-locates it with the payload and allows per-callsite values, at the cost of losing the static-typing guarantee that the fallback is provided.
910
- **Pure docs / no helper.** Just document the orion-style pattern (`useState` + `try/finally` in the user component, `asyncAction` prop with explicit `close`). Rejected: leaves the boilerplate of `useState`/`finally`/no-mutationFn-fallback at every dialog. The user-mentioned `usePending` and "builder for mutationFn" framing made the "no helper" position too thin.
1011
- **Hook in the main entry instead of a subpath.** One import path, more discoverable, tree-shakeable in modern bundlers. Rejected: the hook is ~100–200 bytes brotli; budgeting it into the main entry would push it past the 1 KB limit and force a budget bump (1 KB β†’ ~1.1 KB). Cheap to keep separate, costly to revisit later if subpath gets adopted and we want to fold it back in.
1112

@@ -18,6 +19,9 @@ Issue #22 asks for a first-class answer to *click β†’ run async β†’ keep open on
1819
- **`Callable.mutate(...)` from caller scope is structurally absent**, not deferred. The trigger is local to the user component's render. Adding caller-scope triggering later would require a parallel lib-level channel β€” it is *not* an additive extension of this hook. PR #81 left this door open at the cost of the primitive-on-CallContext shape; we trade that door for the smaller lib core.
1920
- **HMR does not preserve `pending` across saves.** Pending lives in component `useState`; a Fast Refresh during an in-flight mutation resets the visible pending flag (the mutation itself continues in background). Acceptable: editing a dialog mid-mutation is exotic.
2021
- **`MutationCall<Response>` is the public name** of the handler's `call` argument. Chosen over `MutationContext` (which would falsely suggest a React `Context`) and over `Closer` (too narrow once we ever add `signal` / `ended` / etc.).
21-
- **The hook signature uses TypeScript overloads on the options object.** `mutationFn` required β†’ `{ mutationFn }` (no fallback); `mutationFn` possibly-undefined β†’ `{ mutationFn, fallback }` with fallback required. The dead-weight optional fallback field is gone in the required-handler case.
22+
- **The hook signature is positional; the Fallback response is delivered via a chain.** Two TypeScript overloads on the `mutationFn` argument: `mutationFn` required β†’ `submit(payload): void`; `mutationFn` possibly-undefined β†’ `submit(payload): { orEnd(value: Response): void }`. The chain object exists only on the possibly-undefined overload, so attempting `.orEnd` on a required-handler Trigger is a compile-time error. Two exported types describe the shapes: `Trigger<Payload>` (void return) and `ChainTrigger<Payload, Response>` (chain return).
23+
- **The type system no longer enforces the Fallback response.** With the rejected options-object shape, `fallback` was a required field whenever `mutationFn` could be undefined β€” `pnpm check:types` caught omissions. With the chain shape, calling `submit(payload)` without `.orEnd` is a valid program; if `mutationFn` happens to be undefined at runtime, the Trigger is a silent no-op. The escape hatch is intentional (see Manual-close path below); the trade-off is that an actual *omission* now reaches runtime as "the button does nothing on click" instead of failing at compile time.
24+
- **The Fallback response is per-Trigger callsite, not per-Callable.** Distinct buttons in the same component can chain distinct `.orEnd` values aligned with their payload (a Picker chains `.orEnd('A')` on the A button, `.orEnd('B')` on the B button). The rejected options-object shape only exposed a single fallback for all callsites within a single Callable.
25+
- **The Manual-close path is a supported branch, not a footgun.** When `mutationFn` is typed as possibly-undefined and the consumer deliberately omits `.orEnd`, the Call stays open until something else closes it (e.g. a "No" button calling `call.end()` directly). This is a legitimate pattern for dialogs that prefer manual user dismissal when no handler is provided. The same surface-level shape (`submit(payload)` without a chain) covers both the intentional Manual-close and an accidental forgot-to-chain β€” code review is the only signal that distinguishes them.
2226
- **`AbortSignal` on `MutationCall` and `submit.error` on the trigger are deferred**, same as PR #81. Additive without breaking change: extending `MutationCall<Response>` and adding an `error` field to the trigger is purely additive type-wise.
2327
- **The README needs a new section** documenting `react-call/mutation-flow`, distinct from the createCallable basics. The hook isn't part of the "Call your React components" first-impression API; it's an opt-in helper for a specific pattern, and the docs should frame it that way.

0 commit comments

Comments
Β (0)