diff --git a/.changeset/quiet-forms-ignore-stale.md b/.changeset/quiet-forms-ignore-stale.md new file mode 100644 index 0000000000..ed05e7de2a --- /dev/null +++ b/.changeset/quiet-forms-ignore-stale.md @@ -0,0 +1,5 @@ +--- +"@tanstack/form-core": patch +--- + +Ignore aborted async form-level validation results so a slower previous run cannot overwrite newer form state. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d30..3315639d90 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2232,6 +2232,7 @@ export class FormApi< } catch (e: unknown) { rawError = e as ValidationError } + if (controller.signal.aborted) return resolve(undefined) const { formError, fieldErrors: fieldErrorsFromNormalizeError } = normalizeError(rawError) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c57bb044e8..46d18fea48 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -1420,6 +1420,44 @@ describe('form api', () => { }) }) + it('should ignore aborted async form-level validation results', async () => { + vi.useFakeTimers() + + const form = new FormApi({ + defaultValues: { + name: 'valid', + }, + validators: { + onChangeAsyncDebounceMs: 0, + onChangeAsync: async ({ value }) => { + if (value.name === 'slow-invalid') { + await sleep(2000) + return 'Stale validation result' + } + await sleep(50) + return undefined + }, + }, + }) + const field = new FieldApi({ + form, + name: 'name', + }) + form.mount() + field.mount() + + field.setValue('slow-invalid') + await vi.advanceTimersByTimeAsync(0) + field.setValue('valid') + await vi.advanceTimersByTimeAsync(50) + expect(form.state.errors).toEqual([]) + + await vi.advanceTimersByTimeAsync(2000) + expect(form.state.errors).toEqual([]) + expect(form.state.errorMap.onChange).toBeUndefined() + vi.useRealTimers() + }) + it('should run validation onBlur', () => { const form = new FormApi({ defaultValues: {