feat: mutation-flow composition hook (lighter alternative to PR #81)#82
Merged
Conversation
ADR-0014 records the decision to solve issue #22 via a composition hook in a `react-call/mutation-flow` subpath instead of extending `CallContext` with a `call.pending` / `call.mutate` primitive. CONTEXT.md captures the domain glossary the grilling session produced.
`useMutationFlow(call, mutationFn, fallback?)` lives in the new `react-call/mutation-flow` subpath entry. The hook orchestrates the async-submission lifecycle (pending toggle, swallow throws, fallback when no mutationFn) while leaving createCallable and CallContext untouched. Two TypeScript overloads encode the invariant that `fallback` is required iff `mutationFn` may be undefined. Main entry budget unchanged (1 KB brotli). New subpath sits at 271 B ESM / 336 B CJS brotli. See ADR-0014.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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).
Glossary update reflecting the API redesign captured in PR #82 grilling: - Trigger now describes the two callable shapes (void return when MutationFn is required; chain object with `.orEnd` when MutationFn may be undefined). - Fallback response is now delivered at the Trigger callsite via `.orEnd(value)` instead of via the hook's options object. - New term: Manual-close path — the legitimate branch where neither MutationFn nor Fallback response closes the Call (e.g. the consumer wants a "No" button to handle the close). - Relationships and example dialogue updated to reflect the new three-state model. ADR-0014 amendment in a follow-up commit.
desko27
pushed a commit
that referenced
this pull request
May 24, 2026
Glossary update reflecting the API redesign captured in PR #82 grilling: - Trigger now describes the two callable shapes (void return when MutationFn is required; chain object with `.orEnd` when MutationFn may be undefined). - Fallback response is now delivered at the Trigger callsite via `.orEnd(value)` instead of via the hook's options object. - New term: Manual-close path — the legitimate branch where neither MutationFn nor Fallback response closes the Call (e.g. the consumer wants a "No" button to handle the close). - Relationships and example dialogue updated to reflect the new three-state model. ADR-0014 amendment in a follow-up commit.
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.
useMutationFlow goes back to two positional arguments:
useMutationFlow(call, mutationFn)
The Fallback response is now delivered at the Trigger callsite via a
method chain instead of via the hook's options object:
// Required handler
const submit = useMutationFlow(call, mutationFn)
<button onClick={() => submit()}>Yes</button>
// Optional handler — chain .orEnd at the callsite
const submit = useMutationFlow(call, mutationFn)
<button onClick={() => submit().orEnd(true)}>Yes</button>
// Picker — each button chains its own value
<button onClick={() => submit({ choice: 'A' }).orEnd('A')}>A</button>
<button onClick={() => submit({ choice: 'B' }).orEnd('B')}>B</button>
Two TypeScript overloads on the mutationFn argument keep the chain
out of reach when the handler is non-nullable: required → submit
returns void; possibly-undefined → submit returns
`{ orEnd(value): void }`. Two exported types describe the shapes:
`Trigger<Payload>` (void return) and `ChainTrigger<Payload, Response>`
(chain return).
When the handler may be undefined and the callsite omits `.orEnd`,
the call stays open until something else closes it (the Manual-close
path documented in ADR-0014).
New tests cover per-callsite fallback (Picker) and the Manual-close
path. Bundle still well within budget: 268 B brotli ESM (limit 300),
344 B brotli CJS (limit 400).
Disables auto-appended Claude attribution lines on commits and PRs created from this repo.
88247eb to
9ee7b96
Compare
…readmes Shorter, code-first, less prose. Four subsections: main async-handler example, optional handlers via .orEnd, per-button payload+fallback, and the Manual-close path.
6 tasks
6c0e7af to
3f24a75
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #22 with a lighter alternative to PR #81. Ships
useMutationFlowas an opt-in composition hook from a new subpath entryreact-call/mutation-flow, leavingcreateCallableandCallContextuntouched.The
mutationFnreceives a narrowMutationCallview (just{ end }) — noRootPropsleaks into the handler. Throws are swallowed by the trigger so the call stays open for retry.When the
mutationFnparameter is typed as possibly-undefined,submit(payload)returns a chain object whose.orEnd(value)delivers the Fallback response at the callsite. Each button can chain its own value:If the consumer prefers to leave the call open when no
mutationFnis provided (e.g. let a "No" button handle the close), they omit.orEnd— the Manual-close path. See ADR-0014 for the full design rationale, the rejected alternatives (primitive onCallContextPR #81 shape; options-object signature with TS-enforced fallback), and the trade-offs.Domain vocabulary in CONTEXT.md: MutationFlow, MutationFn, MutationCall, Trigger, Fallback response, Manual-close path.
How this compares to #81
createCallablecoreCallContextpending,mutate<Props, Response, Payload, RootProps>)mutationFnlivesCallOptions(2nd arg ofcall()/upsert())mutationFnswap viaCallable.updatePayloadtyping scopecreateCallable(per-Callable)CallOptionsoncall().orEnd(value)(per-button)Callable.mutatefrom caller scopependingacross savesuseState)Bundle impact
dist/main.js— 804 B brotli (unchanged frommain)dist/main.cjs— 885 B brotli (unchanged frommain)dist/mutation-flow.js— 268 B brotli (new, opt-in)dist/mutation-flow.cjs— 344 B brotli (new, opt-in)Consumers who never import the subpath pay zero.
Commits
2a3b897docs(adr): ADR-0014 + CONTEXT.md9b56279feat(react-call): mutation-flow subpath hook (initial implementation)c706ab9refactor(mutation-flow): options-object signature iteration8169fc4docs(context): chain-based fallback delivery + Manual-close pathff5bcd0docs(adr-0014): amend for chain-based signaturedb6846brefactor(mutation-flow): positional signature + chain-based fallback9ee7b96chore(claude): add empty attribution config7e41c5achore(skills): add crafting-effective-readmes from softaworks/agent-toolkit3f24a75docs(readme): rewrite useMutationFlow section via crafting-effective-readmesCommit 3 (options-object iteration) is preserved as part of the design trail recorded in ADR-0014's "Considered options" section.
Out of scope (same deferrals as PR #81)
AbortSignalonMutationCall— additive type-wise, no breaking change to add latersubmit.erroron the trigger — additive without breaking changeCallable.mutate(...)from caller scope — structurally absent in this design (see ADR-0014 "Consequences"); PR feat: async mutation primitive (call.pending + call.mutate) #81 left this door open at the cost of the primitive-on-CallContext shapeTest plan
pnpm vitest run— 87/87 pass (11 inmutation-flow.test.tsx) covering: pending lifecycle, throw-swallow, fallback via.orEnd, per-callsite fallback (Picker A/B), Manual-close path, re-entry guard, payload forwarding, mid-callmutationFnswap, externalendduring pendingpnpm check:types— cleanpnpm lint— cleanpnpm --filter react-call run build— three entries emitted (main, vite, mutation-flow)pnpm --filter react-call size— all four budgets pass (main unchanged at 804/885 B; mutation-flow 268 B ESM / 344 B CJS, below 300/400 B limits)