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
23 changes: 17 additions & 6 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -1766,18 +1774,21 @@ export class FieldApi<
const validatesPromises: Promise<ValidationError | undefined>[] = []
const linkedPromises: Promise<ValidationError | undefined>[] = []

// 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 }))
}
Expand Down
67 changes: 67 additions & 0 deletions packages/form-core/tests/FieldApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
Loading