From 89c5741e4ddb4230e42a0fb982dad2177a1c8aa2 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Fri, 12 Dec 2025 17:36:13 +0800 Subject: [PATCH 1/2] 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 --- .changeset/fix-double-render-keystroke.md | 9 +++++ packages/form-core/src/FieldApi.ts | 34 +++++++++++++------ packages/form-core/tests/FieldApi.spec.ts | 41 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 .changeset/fix-double-render-keystroke.md diff --git a/.changeset/fix-double-render-keystroke.md b/.changeset/fix-double-render-keystroke.md new file mode 100644 index 000000000..58711471f --- /dev/null +++ b/.changeset/fix-double-render-keystroke.md @@ -0,0 +1,9 @@ +--- +"@tanstack/form-core": patch +--- + +fix: prevent unnecessary re-renders when there are no async validators + +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. + +Fixes #1130 diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 0d08bb9f4..d036e32c5 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1728,14 +1728,6 @@ export class FieldApi< >, ) - if (!this.state.meta.isValidating) { - this.setMeta((prev) => ({ ...prev, isValidating: true })) - } - - for (const linkedField of linkedFields) { - linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) - } - /** * We have to use a for loop and generate our promises this way, otherwise it won't be sync * when there are no validators needed to be run @@ -1743,6 +1735,23 @@ export class FieldApi< const validatesPromises: Promise[] = [] const linkedPromises: Promise[] = [] + // Check if there are actual async validators to run before setting isValidating + // 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) + + if (hasAsyncValidators) { + if (!this.state.meta.isValidating) { + this.setMeta((prev) => ({ ...prev, isValidating: true })) + } + + for (const linkedField of linkedFields) { + linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) + } + } + const validateFieldAsyncFn = ( field: AnyFieldApi, validateObj: AsyncValidator, @@ -1845,10 +1854,13 @@ export class FieldApi< await Promise.all(linkedPromises) } - this.setMeta((prev) => ({ ...prev, isValidating: false })) + // Only reset isValidating if we set it to true earlier + if (hasAsyncValidators) { + this.setMeta((prev) => ({ ...prev, isValidating: false })) - for (const linkedField of linkedFields) { - linkedField.setMeta((prev) => ({ ...prev, isValidating: false })) + for (const linkedField of linkedFields) { + linkedField.setMeta((prev) => ({ ...prev, isValidating: false })) + } } return results.filter(Boolean) diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index 320ac1aea..0f3c296a9 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -797,6 +797,47 @@ describe('field api', () => { expect(field.getMeta().errors.length).toBe(0) }) + it('should not toggle isValidating when there are no async validators', async () => { + // Test for https://github.com/TanStack/form/issues/1130 + // Fields were re-rendering twice on each keystroke because isValidating + // was being set to true then false even when there were no async validators + vi.useFakeTimers() + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'name', + // No async validators defined - only sync or none + }) + + field.mount() + + // Track isValidating changes + const isValidatingStates: boolean[] = [] + field.store.subscribe(() => { + isValidatingStates.push(field.getMeta().isValidating) + }) + + // Initial state + expect(field.getMeta().isValidating).toBe(false) + + // Trigger validation by changing value + field.setValue('new value') + await vi.runAllTimersAsync() + + // isValidating should never have been set to true since there are no async validators + // This prevents unnecessary re-renders + expect(isValidatingStates.every((state) => state === false)).toBe(true) + expect(field.getMeta().isValidating).toBe(false) + }) + it('should run async validation onChange', async () => { vi.useFakeTimers() From e77c124a4b3671c149a24006b82fe9650bf016cb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:36:28 +0000 Subject: [PATCH 2/2] ci: apply automated fixes and generate docs --- .changeset/fix-double-render-keystroke.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-double-render-keystroke.md b/.changeset/fix-double-render-keystroke.md index 58711471f..477ab1cde 100644 --- a/.changeset/fix-double-render-keystroke.md +++ b/.changeset/fix-double-render-keystroke.md @@ -1,5 +1,5 @@ --- -"@tanstack/form-core": patch +'@tanstack/form-core': patch --- fix: prevent unnecessary re-renders when there are no async validators