From 98e4e78c52ff2981015fe199b97a508eb29c88cf Mon Sep 17 00:00:00 2001 From: Lucas Patron <86905052+lucas-barake@users.noreply.github.com> Date: Thu, 9 Jul 2026 20:19:41 -0500 Subject: [PATCH 1/2] refactor: use AsyncResult accessors instead of manual result inspection --- packages/form/src/FormAtoms.ts | 25 ++++++++------------ packages/form/test/FormAtoms.test.ts | 35 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/form/src/FormAtoms.ts b/packages/form/src/FormAtoms.ts index 77a89b6..85da539 100644 --- a/packages/form/src/FormAtoms.ts +++ b/packages/form/src/FormAtoms.ts @@ -1,8 +1,8 @@ -import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import { pipe } from "effect/Function" import * as Option from "effect/Option" import * as Schema from "effect/Schema" +import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" import * as Atom from "effect/unstable/reactivity/Atom" import * as Field from "./Field.ts" import * as FormBuilder from "./FormBuilder.ts" @@ -381,21 +381,14 @@ export const make = = Option.none() - if (validationResult._tag === "Failure") { - const parseError = Cause.findErrorOption(validationResult.cause) - if (Option.isSome(parseError) && Schema.isSchemaError(parseError.value)) { - livePerFieldError = Validation.extractFirstError(parseError.value) - } - } + const livePerFieldError = Option.flatMap(AsyncResult.error(validationResult), Validation.extractFirstError) let validationError: Option.Option = Option.none() if (Option.isSome(livePerFieldError)) { validationError = livePerFieldError } else if (Option.isSome(storedError)) { - const isValidating = validationResult.waiting const shouldHideStoredError = storedError.value.source === "field" && - (validationResult._tag === "Success" || isValidating) + (AsyncResult.isSuccess(validationResult) || AsyncResult.isWaiting(validationResult)) if (!shouldHideStoredError) { validationError = Option.some(storedError.value.message) } @@ -799,7 +792,9 @@ export const make = get(internal.validationAtom).waiting).pipe(Atom.setIdleTTL(0)) + const isValidating = Atom.readable((get) => AsyncResult.isWaiting(get(internal.validationAtom))).pipe( + Atom.setIdleTTL(0) + ) const setValueAtom = setValueFamily(fieldKey) @@ -857,7 +852,7 @@ export const make = { - if (get.once(submitAtom).waiting) { + if (AsyncResult.isWaiting(get.once(submitAtom))) { pendingChanges = true return } @@ -888,7 +883,7 @@ export const make = { const result = get.once(submitAtom) - const isSubmitting = result.waiting + const isSubmitting = AsyncResult.isWaiting(result) const justFinished = wasSubmitting && !isSubmitting // Update wasSubmitting BEFORE triggering a follow-up submit. debouncedSubmit // (no debounce) synchronously re-enters this subscription with the new @@ -914,7 +909,7 @@ export const make = = parsedMode.autoSubmit && parsedMode.validation === "onBlur" ? Atom.fnSync()((_: void, get) => { - if (get(submitAtom).waiting) return + if (AsyncResult.isWaiting(get(submitAtom))) return const stateOption = get(stateAtom) if (Option.isNone(stateOption)) return const { lastSubmittedValues, values } = stateOption.value diff --git a/packages/form/test/FormAtoms.test.ts b/packages/form/test/FormAtoms.test.ts index 25ca48e..afcc630 100644 --- a/packages/form/test/FormAtoms.test.ts +++ b/packages/form/test/FormAtoms.test.ts @@ -2045,6 +2045,41 @@ describe("FormAtoms", () => { ) }) + it("hides stored field-source error while validation is re-running", () => { + const runtime = Atom.runtime(Layer.empty) + const NameField = Field.makeField( + "name", + Schema.String.pipe(Schema.decode({ + decode: SchemaGetter.checkEffect(() => Effect.never), + encode: SchemaGetter.passthrough() + })) + ) + const EmailField = Field.makeField("email", Schema.String) + const form = FormBuilder.empty.addField(NameField).addField(EmailField) + const atoms = FormAtoms.make({ runtime, formBuilder: form, onSubmit: () => {}, mode: { validation: "onSubmit" } }) + const registry = AtomRegistry.make() + + let state = atoms.operations.createInitialState({ name: "John", email: "test@test.com" }) + state = atoms.operations.createSubmitState(state) + registry.set(atoms.stateAtom, Option.some(state)) + registry.set(atoms.errorsAtom, new Map([["name", { message: "Name error", source: "field" as const }]])) + + const fieldAtoms = atoms.getOrCreateFieldAtoms("name", NameField.schema) + registry.mount(fieldAtoms.displayErrorAtom) + registry.mount(fieldAtoms.validationAtom) + + expect(registry.get(fieldAtoms.displayErrorAtom)).toEqual(Option.some("Name error")) + + registry.set(fieldAtoms.validationAtom, "John") + + return new Promise((resolve) => + setTimeout(() => { + expect(Option.isNone(registry.get(fieldAtoms.displayErrorAtom))).toBe(true) + resolve() + }, 50) + ) + }) + it("keeps stored refinement error even when validation passes", () => { const runtime = Atom.runtime(Layer.empty) const form = makeTestForm() From 9c792a26ef5290af95bc435df7579e8e1a2cb11b Mon Sep 17 00:00:00 2001 From: Lucas Patron <86905052+lucas-barake@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:28:36 -0500 Subject: [PATCH 2/2] chore: add changeset --- .changeset/asyncresult-accessors.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/asyncresult-accessors.md diff --git a/.changeset/asyncresult-accessors.md b/.changeset/asyncresult-accessors.md new file mode 100644 index 0000000..681b544 --- /dev/null +++ b/.changeset/asyncresult-accessors.md @@ -0,0 +1,5 @@ +--- +"@lucas-barake/effect-form": patch +--- + +use AsyncResult accessors instead of manual result inspection in display-error logic \ No newline at end of file