Improve AI docs
Lint / lint (push) Successful in 25s

This commit is contained in:
Julien Valverdé
2026-08-24 03:13:00 +02:00
parent 60e27bb80f
commit 8cb97625b8
3 changed files with 19 additions and 18 deletions
+2 -2
View File
@@ -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<I>`: 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`.
+14 -14
View File
@@ -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<A, E, R>` |
| mutation result | `mutation.state`, a `View<AsyncResult<A, E>>` |
| `isPending` | `result.waiting` |
| mutation result | `mutation.state`, a `View<{ key: Option<K>; result: AsyncResult<A, E> }>` |
| `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<K>, result: AsyncResult<A, E> }`. `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<E>
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<AsyncResult<A, E>>` | a UI callback that just starts the work |
| `mutate(key)` | the final `FinalMutationState` (`{ key: Option.Some<K>, result: Success \| Failure }`) | an Effect workflow that needs the outcome |
| `mutateView(key)` | a live per-call `View<{ key: Option.Some<K>, result: AsyncResult<A, E> }>` | 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<K>` |
| `latestFinalResult` | latest completed success/failure, `Option` |
| `latestFinalState` | latest completed final state, `Option<FinalMutationState<K, A, E>>` |
| `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
}
```
+3 -2
View File
@@ -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()
</button>
```
- `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