diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 1d5ba3096..43d94385f 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1735,6 +1735,14 @@ export class FieldApi< this.form.options.validationLogic || defaultValidationLogic, }) + // Check if this field has its own async validators BEFORE any await + // This ensures isValidating is set synchronously when async validation is scheduled + // See: https://github.com/TanStack/form/issues/1833 + const hasOwnAsyncValidators = validates.some((v) => v.validate) + if (hasOwnAsyncValidators && !this.state.meta.isValidating) { + this.setMeta((prev) => ({ ...prev, isValidating: true })) + } + // Get the field-specific error messages that are coming from the form's validator const asyncFormValidationResults = await formValidationResultPromise @@ -1766,18 +1774,21 @@ export class FieldApi< const validatesPromises: Promise[] = [] const linkedPromises: Promise[] = [] - // Check if there are actual async validators to run before setting isValidating + // Check if there are actual async validators to run (including linked fields) // This prevents unnecessary re-renders when there are no async validators // See: https://github.com/TanStack/form/issues/1130 - const hasAsyncValidators = - validates.some((v) => v.validate) || - linkedFieldValidates.some((v) => v.validate) + const hasLinkedAsyncValidators = linkedFieldValidates.some( + (v) => v.validate, + ) + const hasAsyncValidators = hasOwnAsyncValidators || hasLinkedAsyncValidators - if (hasAsyncValidators) { + // Set isValidating for linked fields and current field when linked fields are validating + // This preserves original behavior where current field shows validating + // when any of its linked fields are validating + if (hasLinkedAsyncValidators) { if (!this.state.meta.isValidating) { this.setMeta((prev) => ({ ...prev, isValidating: true })) } - for (const linkedField of linkedFields) { linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) } diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index abca29868..e3e5374c5 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -2936,4 +2936,71 @@ describe('edge cases and error handling', () => { field.handleChange(undefined) expect(field.state.value).toBeUndefined() }) + + // Test for https://github.com/TanStack/form/issues/1833 + it('should keep isValidating true during async debounce period when sync validation passes', async () => { + vi.useFakeTimers() + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'name', + validators: { + // Sync validator that passes for 'error' value + onChange: ({ value }) => { + if (value.length < 3) return 'Too short' + return undefined + }, + // Async validator with 500ms debounce + onChangeAsyncDebounceMs: 500, + onChangeAsync: async ({ value }) => { + await sleep(100) + if (value === 'error') return 'Server error' + return undefined + }, + }, + }) + + field.mount() + + // Initially not validating + expect(field.getMeta().isValidating).toBe(false) + expect(form.state.canSubmit).toBe(true) + + // Touch the field first + field.setMeta((prev) => ({ ...prev, isTouched: true })) + + // Type a value that passes sync validation + field.setValue('error') + + // Immediately after setValue, isValidating should be true + // because async validation is pending (even though debounce hasn't elapsed) + expect(field.getMeta().isValidating).toBe(true) + + // Form's canSubmit should be false during validation + expect(form.state.canSubmit).toBe(false) + + // Advance past debounce but not past async validator + await vi.advanceTimersByTimeAsync(500) + + // Still validating (async validator is running) + expect(field.getMeta().isValidating).toBe(true) + expect(form.state.canSubmit).toBe(false) + + // Finish all timers + await vi.runAllTimersAsync() + + // Now validation is complete + expect(field.getMeta().isValidating).toBe(false) + expect(field.getMeta().errors).toContain('Server error') + + vi.useRealTimers() + }) })