From da0d427b2f8c4f1a3c48281499aecaf564cdae62 Mon Sep 17 00:00:00 2001 From: Marc Hermann Date: Wed, 15 Apr 2026 08:43:47 -0400 Subject: [PATCH] fix: revalidate nested fields after parent object updates - Fixes the stale nested field meta described in #2113. - When `setFieldValue` updates a parent object field like `a`, mounted descendant fields like `a.b` are now revalidated on change as well. This keeps nested field-level errors in sync after programmatic parent object updates. - Added a regression test covering the reported case where updating `a` from `{ b: 0 }` to `{ b: 1 }` should clear the existing validation error on `a.b`. Fixes #2113 --- .changeset/stale-nested-field-meta.md | 5 ++++ packages/form-core/src/FormApi.ts | 15 +++++++++++ packages/form-core/tests/FormApi.spec.ts | 34 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 .changeset/stale-nested-field-meta.md diff --git a/.changeset/stale-nested-field-meta.md b/.changeset/stale-nested-field-meta.md new file mode 100644 index 000000000..d8c6aee6d --- /dev/null +++ b/.changeset/stale-nested-field-meta.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Fix stale nested field errors when setting a parent object field by revalidating mounted descendant fields after the parent value changes. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index d78c39a8a..c997b530b 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2284,6 +2284,17 @@ export class FormApi< const dontUpdateMeta = opts?.dontUpdateMeta ?? false const dontRunListeners = opts?.dontRunListeners ?? false const dontValidate = opts?.dontValidate ?? false + const fieldString = field.toString() + const descendantFields = Object.keys(this.fieldInfo).filter((fieldKey) => { + if (fieldKey === fieldString) { + return false + } + + return ( + fieldKey.startsWith(`${fieldString}.`) || + fieldKey.startsWith(`${fieldString}[`) + ) + }) as DeepKeys[] batch(() => { if (!dontUpdateMeta) { @@ -2313,6 +2324,10 @@ export class FormApi< if (!dontValidate) { this.validateField(field, 'change') + + descendantFields.forEach((descendantField) => { + this.getFieldInfo(descendantField).instance?.validate('change') + }) } } diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c1b36a85c..ed8e043f7 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -225,6 +225,40 @@ describe('form api', () => { expect(form.getFieldValue('name')).toEqual('other') }) + it('should clear nested field errors when setting a parent object field', () => { + const form = new FormApi({ + defaultValues: { + a: { + b: 0, + }, + }, + }) + + const childField = new FieldApi({ + form, + name: 'a.b', + validators: { + onChange: ({ value }) => + value > 0 ? undefined : 'Must be greater than 0', + }, + }) + + form.mount() + childField.mount() + + childField.setValue(0) + + expect(childField.state.meta.errors).toEqual(['Must be greater than 0']) + + form.setFieldValue('a', { + b: 1, + }) + + expect(form.getFieldValue('a.b')).toBe(1) + expect(childField.state.meta.errors).toEqual([]) + expect(childField.state.meta.isValid).toBe(true) + }) + it("should be dirty after a field's value has been set", () => { const form = new FormApi({ defaultValues: {