You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(mutation-flow): options-object signature for useMutationFlow
Replace positional `useMutationFlow(call, mutationFn, fallback?)` with
`useMutationFlow(call, { mutationFn, fallback? })`. Same two-overload
shape encodes the "fallback required iff mutationFn may be undefined"
invariant on the options object instead of on positional arity.
Docs (README, ADR-0014, CONTEXT.md, changeset) and tests updated to the
new shape. Bundle still within budgets (275 B / 353 B brotli).
Copy file name to clipboardExpand all lines: .changeset/feat-mutation-flow.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
"react-call": minor
3
3
---
4
4
5
-
-**New `react-call/mutation-flow` subpath entry** — `useMutationFlow(call, mutationFn, fallback?)` is an opt-in hook that wraps the canonical async-submission flow (click → run async → keep open on failure, end on success). The main `react-call` entry stays unchanged: bundle size and API surface of `createCallable` / `CallContext` are not affected. Consumers who never import the subpath pay zero.
5
+
-**New `react-call/mutation-flow` subpath entry** — `useMutationFlow(call, { mutationFn, fallback? })` is an opt-in hook that wraps the canonical async-submission flow (click → run async → keep open on failure, end on success). The main `react-call` entry stays unchanged: bundle size and API surface of `createCallable` / `CallContext` are not affected. Consumers who never import the subpath pay zero.
The `mutationFn` receives a narrow `MutationCall<Response>` view (`{ end }`) — no `RootProps` ever leaks into the handler's signature. Throws are swallowed by the trigger so the call stays open for retry; the handler decides when (if ever) to `call.end()`. A 2-arg overload applies when the `mutationFn`parameter is required; a 3-arg overload requires a `fallback`response when `mutationFn` may be undefined. See [ADR-0014](docs/adr/0014-mutation-flow-as-composition-hook.md) for the design rationale and the trade-off versus making this a primitive on `CallContext`.
36
+
The `mutationFn` receives a narrow `MutationCall<Response>` view (`{ end }`) — no `RootProps` ever leaks into the handler's signature. Throws are swallowed by the trigger so the call stays open for retry; the handler decides when (if ever) to `call.end()`. The options object has two overloads: `{ mutationFn }` when the parameter is required, and `{ mutationFn, fallback }` (with `fallback`required by the type system) when `mutationFn` may be undefined. See [ADR-0014](docs/adr/0014-mutation-flow-as-composition-hook.md) for the design rationale and the trade-off versus making this a primitive on `CallContext`.
37
37
38
38
-**Exports**: `MutationFn`, `MutationCall`, `Trigger`, `useMutationFlow` from `react-call/mutation-flow`.
Copy file name to clipboardExpand all lines: docs/adr/0014-mutation-flow-as-composition-hook.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@ Issue #22 asks for a first-class answer to *click → run async → keep open on
5
5
## Considered options
6
6
7
7
-**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 encode the invariant "fallback is required iff `mutationFn` is possibly-undefined", so the wart of a dead-weight third arg is closed at compile time. Lib core: zero changes.
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.
9
9
-**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.
10
10
-**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.
11
11
@@ -18,6 +18,6 @@ Issue #22 asks for a first-class answer to *click → run async → keep open on
18
18
-**`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.
19
19
-**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.
20
20
-**`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.**`mutationFn` required → 2-arg signature with no fallback; `mutationFn` possibly-undefined → 3-arg signature with fallback required. The dead-weight third arg from the original sketch is gone.
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
22
-**`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.
23
23
-**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