Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-stale-onmount-linked-revalidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/form-core": patch
---

fix(form-core): clear a stale field-level `onMount` error when a linked-field revalidation (`onChangeListenTo`/`onBlurListenTo`) passes, so the field is no longer left permanently invalid
29 changes: 28 additions & 1 deletion packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ export class FieldApi<
const validateFieldFn = (
field: AnyFieldApi,
validateObj: SyncValidator<any>,
isLinkedField = false,
) => {
const errorMapKey = getErrorMapKey(validateObj.cause)

Expand Down Expand Up @@ -1334,14 +1335,40 @@ export class FieldApi<
if (newErrorValue) {
hasErrored = true
}

// A direct value change clears the mount-time error in
// `setFieldValue`. When a field is revalidated indirectly via
// `onChangeListenTo`/`onBlurListenTo`, its own value never changed, so
// that clearing never ran and a now-stale `onMount` error would linger
// forever (leaving the field permanently invalid even though its
// validators now pass). Clear it once a passing linked revalidation
// supersedes it.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (
isLinkedField &&
!newErrorValue &&
field.state.meta.errorMap?.onMount
) {
field.setMeta((prev) => ({
...prev,
errorMap: {
...prev.errorMap,
onMount: undefined,
},
errorSourceMap: {
...prev.errorSourceMap,
onMount: undefined,
},
}))
}
}

for (const validateObj of validates) {
validateFieldFn(this, validateObj)
}
for (const fieldValitateObj of linkedFieldValidates) {
if (!fieldValitateObj.validate) continue
validateFieldFn(fieldValitateObj.field, fieldValitateObj)
validateFieldFn(fieldValitateObj.field, fieldValitateObj, true)
}
})

Expand Down
51 changes: 51 additions & 0 deletions packages/form-core/tests/FieldApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,57 @@ describe('field api', () => {
])
})

it('should clear a stale onMount error when a linked field revalidation passes', () => {
const form = new FormApi({
defaultValues: {
password: '',
confirm_password: 'password',
},
})

form.mount()

const passField = new FieldApi({
form,
name: 'password',
})

const passconfirmField = new FieldApi({
form,
name: 'confirm_password',
validators: {
onMount: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue('password')
? 'Passwords do not match'
: undefined,
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue('password')
? 'Passwords do not match'
: undefined,
},
})

passField.mount()
passconfirmField.mount()

// onMount reports the mismatch (password === '', confirm === 'password').
expect(passconfirmField.state.meta.errorMap.onMount).toBe(
'Passwords do not match',
)
expect(passconfirmField.getMeta().isValid).toBe(false)

// Changing the linked field re-runs confirm's onChange (which now passes)
// without touching confirm's own value. The stale onMount error should
// clear, just as it would if the field's value had changed directly.
passField.setValue('password')

expect(passconfirmField.state.meta.errorMap.onMount).toBeUndefined()
expect(passconfirmField.state.meta.errors).toStrictEqual([])
expect(passconfirmField.getMeta().isValid).toBe(true)
expect(form.state.isValid).toBe(true)
})

it('should run onBlur on a linked field', () => {
const form = new FormApi({
defaultValues: {
Expand Down