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/fix-stale-onblur-resubmit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

fix(form-core): clear stale onBlur errors on re-submission
19 changes: 13 additions & 6 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2105,12 +2105,19 @@ export class FormApi<
submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta)

if (!this.state.canSubmit && !this._devtoolsSubmissionOverride) {
this.options.onSubmitInvalid?.({
value: this.state.values,
formApi: this,
meta: submitMetaArg,
})
return
// On re-submission (submissionAttempts > 1), skip the early return so
// validateAllFields can re-run and clear stale field errors (e.g. from a
// previous onBlur validation that is no longer relevant). The
// isFieldsValid check below will call onSubmitInvalid if the form is
// still invalid after re-validation.
if (this.baseStore.state.submissionAttempts <= 1) {
this.options.onSubmitInvalid?.({
value: this.state.values,
formApi: this,
meta: submitMetaArg,
})
return
}
}

this.baseStore.setState((d) => ({ ...d, isSubmitting: true }))
Expand Down
47 changes: 47 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,53 @@ describe('form api', () => {
expect(formSubmit).toHaveBeenCalledOnce()
})

it('should run field-level onBlur validators on re-submission to clear stale errors', async () => {
// Regression: stale onBlur errors could prevent re-submission because
// canSubmit became false and _handleSubmit returned early before running
// validateAllFields, which would have re-evaluated and cleared the error.
// See: https://github.com/TanStack/form/issues/2034
const onSubmit = vi.fn()
const onSubmitInvalid = vi.fn()

const form = new FormApi({
defaultValues: { type: 'PIN', pin: '' },
onSubmit,
onSubmitInvalid,
})
form.mount()

// PIN field with an onBlur validator that is only required when type === 'PIN'
const pinField = new FieldApi({
form,
name: 'pin',
validators: {
onBlur: ({ value }) =>
form.getFieldValue('type') === 'PIN' && !value
? 'PIN is required'
: undefined,
},
})
pinField.mount()

// Simulate user touching and blurring the PIN field while type is 'PIN'
pinField.handleBlur()
expect(pinField.state.meta.errorMap.onBlur).toBe('PIN is required')

// First submit: form is invalid, onSubmitInvalid is called
await form.handleSubmit()
expect(onSubmitInvalid).toHaveBeenCalledTimes(1)
expect(onSubmit).not.toHaveBeenCalled()

// User switches type to 'Card' — PIN is no longer required
form.setFieldValue('type', 'Card')

// Second submit: the onBlur error is stale (PIN field still has it), but
// re-running validators on submit should clear it since type !== 'PIN'
await form.handleSubmit()
expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmitInvalid).toHaveBeenCalledTimes(1) // not called again
})

it('should run all types of async validation on fields during submit', async () => {
vi.useFakeTimers()

Expand Down