From 8cb97625b8d8d080b12e87e751eadc6aaa6341b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 24 Aug 2026 03:13:00 +0200 Subject: [PATCH] Improve AI docs --- packages/effect-view/ai-docs/Form.md | 4 +-- packages/effect-view/ai-docs/Mutation.md | 28 ++++++++++---------- packages/effect-view/ai-docs/MutationForm.md | 5 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/effect-view/ai-docs/Form.md b/packages/effect-view/ai-docs/Form.md index f52a06c..432429d 100644 --- a/packages/effect-view/ai-docs/Form.md +++ b/packages/effect-view/ai-docs/Form.md @@ -49,8 +49,8 @@ const input = yield* Form.useInput(emailField, { debounce: "250 millis" }) ``` - `Form.useInput(form, { debounce? })` returns `{ value, setValue }` from the subform's encoded value. `setValue` writes the form and re-runs the schema pipeline. `debounce` delays propagation to the form (the displayed value updates immediately) — useful for text inputs to avoid validating every keystroke. -- `Form.useOptionalInput(form, options?)` is for an encoded `Option` field: returns `{ value, setValue, enabled, setEnabled }` for a togglable optional input. -- `Form.useStatus(form)` returns `{ isValidating, isCommitting, canCommit }`, debounced to avoid flicker in pending indicators. +- `Form.useOptionalInput(form, { defaultValue, debounce? })` is for a field whose encoded value is `Option`: returns `{ value, setValue, enabled, setEnabled }` for a togglable optional input. `value`/`setValue` operate on the unwrapped `I`; `defaultValue` is used as `value` while `enabled` is `false` (i.e. while the encoded field is `None`), and `defaultValue` is required. +- `Form.useStatus(form, { debounce? })` returns `{ isValidating, isCommitting, canCommit }`, debounced (default 250ms) to avoid flicker in pending indicators. These hooks are building blocks for your own reusable input components — wrap them once to handle labels, issues, disabled state, and styling consistently; both hooks accept any `Form.Form`, so the same input component works with subforms from `MutationForm` or `LensForm`. diff --git a/packages/effect-view/ai-docs/Mutation.md b/packages/effect-view/ai-docs/Mutation.md index 13ffbb1..919837b 100644 --- a/packages/effect-view/ai-docs/Mutation.md +++ b/packages/effect-view/ai-docs/Mutation.md @@ -6,8 +6,8 @@ effect-view's counterpart to TanStack Query mutations: user-triggered asynchrono |---|---| | mutation variables | the input key `K` | | `mutationFn` | `f: (key: K) => Effect` | -| mutation result | `mutation.state`, a `View>` | -| `isPending` | `result.waiting` | +| mutation result | `mutation.state`, a `View<{ key: Option; result: AsyncResult }>` | +| `isPending` | `state.result.waiting` | | `mutateAsync` | `mutation.mutate(key)` | | start without awaiting | `mutation.mutateView(key)` | @@ -25,14 +25,14 @@ const mutation = yield* Component.useOnMount(() => ## AsyncResult state -`mutation.state` starts `Initial` (`waiting: false`); calling `mutate`/`mutateView` sets `waiting: true`, then publishes `Success` or `Failure`. +`mutation.state` is a `View` of `{ key: Option, result: AsyncResult }`. `result` starts `Initial` (`waiting: false`); calling `mutate`/`mutateView` sets `waiting: true`, then publishes `Success` or `Failure`. Match on `state.result`, not `state` itself: ```tsx import { AsyncResult } from "effect/unstable/reactivity" -const [result] = yield* View.useAll([mutation.state]) +const [state] = yield* View.useAll([mutation.state]) -AsyncResult.match(result, { +AsyncResult.match(state.result, { onInitial: ({ waiting }) => (...), onFailure: ({ cause, previousSuccess, waiting }) => (...), // cause: Cause onSuccess: ({ value, waiting }) => (...), @@ -45,14 +45,14 @@ AsyncResult.match(result, { | Method | Returns | Use for | |---|---|---| -| `mutate(key)` | the final `Success`/`Failure` | an Effect workflow that needs the outcome | -| `mutateView(key)` | a live per-call `View>` | a UI callback that just starts the work | +| `mutate(key)` | the final `FinalMutationState` (`{ key: Option.Some, result: Success \| Failure }`) | an Effect workflow that needs the outcome | +| `mutateView(key)` | a live per-call `View<{ key: Option.Some, result: AsyncResult }>` | a UI callback that just starts the work | ```tsx const runPromise = yield* Component.useRunPromise() void runPromise(Effect.gen(function* () { - const result = yield* mutation.mutate(input) - if (AsyncResult.isSuccess(result)) yield* Effect.log(`Saved ${result.value.id}`) + const final = yield* mutation.mutate(input) + if (AsyncResult.isSuccess(final.result)) yield* Effect.log(`Saved ${final.result.value.id}`) })) ``` @@ -61,7 +61,7 @@ const runSync = yield* Component.useRunSync() const state = runSync(mutation.mutateView(input)) // a View for this specific call ``` -The mutation Effect never fails with `E` itself — it captures the operation's `Exit` and always resolves to a final `AsyncResult.Success`/`Failure`. +The mutation Effect never fails with `E` itself — it captures the operation's `Exit` and always resolves to a final state wrapping an `AsyncResult.Success`/`Failure`. ## Reactive metadata @@ -69,7 +69,7 @@ The mutation Effect never fails with `E` itself — it captures the operation's |---|---| | `state` | latest mutation state, shared `View` | | `latestKey` | most recent input, `Option` | -| `latestFinalResult` | latest completed success/failure, `Option` | +| `latestFinalState` | latest completed final state, `Option>` | | `fiber` | most recently started mutation fiber, `Option` | ## Concurrency @@ -81,9 +81,9 @@ Starting a mutation does not interrupt an earlier one — calls can overlap, eac Mutations never auto-invalidate `Query` caches — compose it explicitly: ```tsx -const result = yield* updatePost.mutate(input) -if (AsyncResult.isSuccess(result)) { - yield* posts.invalidateCacheEntry(["post", result.value.id] as const) +const final = yield* updatePost.mutate(input) +if (AsyncResult.isSuccess(final.result)) { + yield* posts.invalidateCacheEntry(["post", final.result.value.id] as const) yield* posts.refreshView // invalidation alone does not refetch } ``` diff --git a/packages/effect-view/ai-docs/MutationForm.md b/packages/effect-view/ai-docs/MutationForm.md index d082a3e..1af1d82 100644 --- a/packages/effect-view/ai-docs/MutationForm.md +++ b/packages/effect-view/ai-docs/MutationForm.md @@ -10,7 +10,7 @@ const form = yield* Component.useOnMount(() => MutationForm.make({ schema: ProfileSchema, initialEncodedValue: { displayName: "", age: "", contact: { email: "" } }, - f: ([profile]) => Effect.log(`Creating ${profile.displayName}, age ${profile.age}`), + f: ([profile, form]) => Effect.log(`Creating ${profile.displayName}, age ${profile.age}`), }).pipe(MutationForm.thenRun), ) @@ -22,10 +22,11 @@ const runPromise = yield* Component.useRunPromise() ``` -- `MutationForm.make({ schema, initialEncodedValue, f })` constructs the form; `f` receives the decoded value(s) as its mutation input (`profile.age` is a number even though the input edited a string). `MutationForm.thenRun` starts initial validation in the current scope. +- `MutationForm.make({ schema, initialEncodedValue, f })` constructs the form; the underlying `Mutation`'s input key is the tuple `[decodedValue, form]`, not just the decoded value. Most `f` implementations only destructure the first element (`profile.age` is a number even though the input edited a string); `form` is included so `f` can, if needed, read other form state (e.g. `form.issues`, `form.encodedValue`) while handling the submission. `MutationForm.thenRun` starts initial validation in the current scope. - Create once and keep stable — the usual home is `Component.useOnMount`. - `form.submit` runs the mutation only when the form can currently commit; schema issues block submission before the mutation ever runs. - Focus into subforms/fields with `Form.focusObjectOn`/`focusArrayAt`/etc. (see `Form.md`) and bind them to inputs with `Form.useInput`. +- If `f` fails with a `Schema.SchemaError`, `form.submit` formats that error into `form.issues` automatically — the same formatting path used for client-side decoding errors — instead of only surfacing it as a mutation failure. Any other failure from `f` is left as the mutation's `Failure` and does not touch `form.issues`. ## Example: schema-owned date conversion