Skip to content

Commit c753d5e

Browse files
fix(form-core): prevent double re-render when no async validators (#1929)
* fix(form-core): prevent double re-render when no async validators Fields were re-rendering twice on each keystroke because `isValidating` was being toggled (true -> false) even when there were no async validators. This fix checks if there are actual async validators before setting `isValidating` state, preventing unnecessary re-renders. Fixes #1130 * ci: apply automated fixes and generate docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent e15267c commit c753d5e

File tree

3 files changed

+73
-11
lines changed

3 files changed

+73
-11
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@tanstack/form-core': patch
3+
---
4+
5+
fix: prevent unnecessary re-renders when there are no async validators
6+
7+
Fields were re-rendering twice on each keystroke because `isValidating` was being set to `true` then `false` even when there were no async validators to run. This fix checks if there are actual async validators before toggling the `isValidating` state.
8+
9+
Fixes #1130

packages/form-core/src/FieldApi.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,21 +1728,30 @@ export class FieldApi<
17281728
>,
17291729
)
17301730

1731-
if (!this.state.meta.isValidating) {
1732-
this.setMeta((prev) => ({ ...prev, isValidating: true }))
1733-
}
1734-
1735-
for (const linkedField of linkedFields) {
1736-
linkedField.setMeta((prev) => ({ ...prev, isValidating: true }))
1737-
}
1738-
17391731
/**
17401732
* We have to use a for loop and generate our promises this way, otherwise it won't be sync
17411733
* when there are no validators needed to be run
17421734
*/
17431735
const validatesPromises: Promise<ValidationError | undefined>[] = []
17441736
const linkedPromises: Promise<ValidationError | undefined>[] = []
17451737

1738+
// Check if there are actual async validators to run before setting isValidating
1739+
// This prevents unnecessary re-renders when there are no async validators
1740+
// See: https://github.com/TanStack/form/issues/1130
1741+
const hasAsyncValidators =
1742+
validates.some((v) => v.validate) ||
1743+
linkedFieldValidates.some((v) => v.validate)
1744+
1745+
if (hasAsyncValidators) {
1746+
if (!this.state.meta.isValidating) {
1747+
this.setMeta((prev) => ({ ...prev, isValidating: true }))
1748+
}
1749+
1750+
for (const linkedField of linkedFields) {
1751+
linkedField.setMeta((prev) => ({ ...prev, isValidating: true }))
1752+
}
1753+
}
1754+
17461755
const validateFieldAsyncFn = (
17471756
field: AnyFieldApi,
17481757
validateObj: AsyncValidator<any>,
@@ -1845,10 +1854,13 @@ export class FieldApi<
18451854
await Promise.all(linkedPromises)
18461855
}
18471856

1848-
this.setMeta((prev) => ({ ...prev, isValidating: false }))
1857+
// Only reset isValidating if we set it to true earlier
1858+
if (hasAsyncValidators) {
1859+
this.setMeta((prev) => ({ ...prev, isValidating: false }))
18491860

1850-
for (const linkedField of linkedFields) {
1851-
linkedField.setMeta((prev) => ({ ...prev, isValidating: false }))
1861+
for (const linkedField of linkedFields) {
1862+
linkedField.setMeta((prev) => ({ ...prev, isValidating: false }))
1863+
}
18521864
}
18531865

18541866
return results.filter(Boolean)

packages/form-core/tests/FieldApi.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,47 @@ describe('field api', () => {
797797
expect(field.getMeta().errors.length).toBe(0)
798798
})
799799

800+
it('should not toggle isValidating when there are no async validators', async () => {
801+
// Test for https://github.com/TanStack/form/issues/1130
802+
// Fields were re-rendering twice on each keystroke because isValidating
803+
// was being set to true then false even when there were no async validators
804+
vi.useFakeTimers()
805+
806+
const form = new FormApi({
807+
defaultValues: {
808+
name: 'test',
809+
},
810+
})
811+
812+
form.mount()
813+
814+
const field = new FieldApi({
815+
form,
816+
name: 'name',
817+
// No async validators defined - only sync or none
818+
})
819+
820+
field.mount()
821+
822+
// Track isValidating changes
823+
const isValidatingStates: boolean[] = []
824+
field.store.subscribe(() => {
825+
isValidatingStates.push(field.getMeta().isValidating)
826+
})
827+
828+
// Initial state
829+
expect(field.getMeta().isValidating).toBe(false)
830+
831+
// Trigger validation by changing value
832+
field.setValue('new value')
833+
await vi.runAllTimersAsync()
834+
835+
// isValidating should never have been set to true since there are no async validators
836+
// This prevents unnecessary re-renders
837+
expect(isValidatingStates.every((state) => state === false)).toBe(true)
838+
expect(field.getMeta().isValidating).toBe(false)
839+
})
840+
800841
it('should run async validation onChange', async () => {
801842
vi.useFakeTimers()
802843

0 commit comments

Comments
 (0)