Files
effect-view/packages/effect-view/ai-docs/MutationForm.md
T
ThilawynandJulien Valverdé 0e2b4c1951
Publish / publish (push) Successful in 2m6s
Lint / lint (push) Successful in 1m3s
Improve effect-view docs, mutation keys, and Fast Refresh (#76)
## Summary

- Add comprehensive AI-oriented documentation for effect-view components, state, async rendering, queries, mutations, forms, streams, and runtime setup.
- Fix `Mutation.mutate` so each invocation consistently uses its own key instead of reusing the previous mutation key.
- Improve Vite Fast Refresh instrumentation for:
  - User-defined component wrappers.
  - Data-first and pipeline-based `withContext`/`withRuntime` entrypoints.
  - Nested function handling.
  - Stable hook signatures that preserve state for non-hook-related edits.
- Add regression tests for mutation keys and refresh behavior.
- Bump `effect-view` to `0.1.5` and `@effect-view/vite-plugin` to `0.0.2`.

## Testing

- `bun run --cwd packages/effect-view test -- src/Mutation.test.ts`
  - 4 tests passed
- `bun run --cwd packages/vite-plugin test -- src/plugin.test.ts`
  - 11 tests passed

The full test suite was not run.

---------

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #76
2026-08-26 01:06:03 +02:00

3.2 KiB

MutationForm

A root form (implements Form.Form, see Form.md) that owns a local encoded draft and passes the valid decoded value to a Mutation when submit runs. Use for registration, checkout, search — any workflow with an explicit submit action.

import { Effect } from "effect"
import { Component, MutationForm, View } from "effect-view"

const form = yield* Component.useOnMount(() =>
  MutationForm.make({
    schema: ProfileSchema,
    initialEncodedValue: { displayName: "", age: "", contact: { email: "" } },
    f: ([profile, form]) => Effect.log(`Creating ${profile.displayName}, age ${profile.age}`),
  }).pipe(MutationForm.thenRun),
)

const [canCommit, isCommitting] = yield* View.useAll([form.canCommit, form.isCommitting])
const runPromise = yield* Component.useRunPromise()

<button disabled={!canCommit || isCommitting} onClick={() => void runPromise(form.submit)}>
  {isCommitting ? "Creating..." : "Create profile"}
</button>
  • 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

class DateTimeUtcFromZoned extends Schema.transformOrFail(Schema.DateTimeZonedFromSelf, Schema.DateTimeUtcFromSelf, {
  strict: true,
  decode: input => ParseResult.succeed(DateTime.toUtc(input)),
  encode: DateTime.setZoneCurrent,
}) {}

export class DateTimeUtcFromZonedInput extends Schema.transformOrFail(Schema.String, DateTimeUtcFromZoned, {
  strict: true,
  decode: (input, _options, ast) => Effect.flatMap(DateTime.CurrentTimeZone, timeZone =>
    Option.match(DateTime.makeZoned(input, { timeZone, adjustForTimeZone: true }), {
      onSome: ParseResult.succeed,
      onNone: () => ParseResult.fail(new ParseResult.Type(ast, input, "Enter a valid date and time")),
    })),
  encode: value => ParseResult.succeed(DateTime.formatIsoZoned(value).slice(0, 16)),
}) {}

An <input type="datetime-local"> edits "2026-07-22T14:30"; the schema decodes it to a UTC DateTime.Utc (decode(...).pipe requires DateTime.CurrentTimeZone — provide DateTime.layerCurrentZoneLocal in the runtime) and the mutation receives the UTC instant directly. No manual date parsing in the component.