From 9f412729da7019c8ef8c529c767de6c9426c7286 Mon Sep 17 00:00:00 2001 From: Shuang Wu Date: Thu, 14 May 2026 21:19:14 -0500 Subject: [PATCH] fix(form-core): clear onSubmit error for fields without associated instance --- .changeset/salty-carpets-follow.md | 5 ++++ packages/form-core/src/FormApi.ts | 17 ++++++++++++++ packages/form-core/tests/FormApi.spec.ts | 29 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 .changeset/salty-carpets-follow.md diff --git a/.changeset/salty-carpets-follow.md b/.changeset/salty-carpets-follow.md new file mode 100644 index 000000000..620d28351 --- /dev/null +++ b/.changeset/salty-carpets-follow.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +clear onSubmit error for fields without associated instance diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index c12a0cfca..f706a99c9 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1655,6 +1655,23 @@ export class FormApi< if (!fieldInstance) { const { hasErrored } = this.validateSync(cause) + // Clear stale onSubmit errors on this field when validation passes, + // mirroring what FieldApi.validateSync does for mounted fields. + if (!hasErrored && cause !== 'submit') { + const submitErrKey = getErrorMapKey('submit') + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.getFieldMeta(field)?.errorMap?.[submitErrKey]) { + this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ + ...prev, + errorMap: { ...prev.errorMap, [submitErrKey]: undefined }, + errorSourceMap: { + ...prev.errorSourceMap, + [submitErrKey]: undefined, + }, + })) + } + } + if (hasErrored && !this.options.asyncAlways) { return this.getFieldMeta(field)?.errors ?? [] } diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 092d0ed6c..3a8dd9def 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -2568,6 +2568,35 @@ describe('form api', () => { expect(form.state.errors).toStrictEqual([]) }) + it('should clear onSubmit field errors via setFieldValue without field instance mounted', async () => { + const validateMessage = 'first name is required' + const form = new FormApi({ + defaultValues: { firstName: '' }, + validators: { + onSubmit: ({ value }) => { + if (value.firstName.length === 0) + return { fields: { firstName: validateMessage } } + return null + }, + }, + }) + + form.mount() + + await form.handleSubmit() + expect(form.state.isFieldsValid).toBe(false) + expect(form.state.canSubmit).toBe(false) + expect(form.state.fieldMeta.firstName?.errorMap.onSubmit).toBe( + validateMessage, + ) + + form.setFieldValue('firstName', 'John') + + expect(form.state.fieldMeta.firstName?.errorMap.onSubmit).toBeUndefined() + expect(form.state.isFieldsValid).toBe(true) + expect(form.state.canSubmit).toBe(true) + }) + it('should run validators in order form sync -> field sync -> form async -> field async', async () => { const order: string[] = [] const formAsyncChange = vi.fn().mockImplementation(async () => {