From 4567071ee9d1f59579df7d93bfb8b9d87e2a32cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EA=B7=9C=EC=A7=84?= Date: Sun, 25 Jan 2026 23:13:51 +0900 Subject: [PATCH 1/5] fix(form-core): unify prioritized default logic for isDefaultValue and reset --- .changeset/gentle-jars-share.md | 5 ++ packages/form-core/src/FormApi.ts | 60 +++++++++++++++-------- packages/form-core/tests/FieldApi.spec.ts | 50 ++++++++++++++++++- 3 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 .changeset/gentle-jars-share.md diff --git a/.changeset/gentle-jars-share.md b/.changeset/gentle-jars-share.md new file mode 100644 index 000000000..e4de6dfc1 --- /dev/null +++ b/.changeset/gentle-jars-share.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': minor +--- + +Introduced a **Prioritized Default System** that ensures consistency between field metadata and form reset behavior. This change prioritizes field-level default values over form-level defaults across `isDefaultValue` derivation, `form.reset()`, and `form.resetField()`. This ensures that field metadata accurately reflects the state the form would return to upon reset and prevents `undefined` from being incorrectly treated as a default when a value is explicitly specified. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index ff52bbe74..702501e9f 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1091,16 +1091,11 @@ export class FormApi< // As primitives, we don't need to aggressively persist the same referential value for performance reasons const isFieldValid = !isNonEmptyArray(fieldErrors) const isFieldPristine = !currBaseMeta.isDirty - const isDefaultValue = - evaluate( - curFieldVal, + const isDefaultValue = evaluate( + curFieldVal, + this.getFieldInfo(fieldName)?.instance?.options.defaultValue ?? getBy(this.options.defaultValues, fieldName), - ) || - evaluate( - curFieldVal, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.getFieldInfo(fieldName)?.instance?.options.defaultValue, - ) + ) if ( prevFieldInfo && @@ -1507,16 +1502,35 @@ export class FormApi< } } - this.baseStore.setState(() => - getDefaultFormState({ + this.baseStore.setState(() => { + let nextValues = + values ?? + this.options.defaultValues ?? + this.options.defaultState?.values + + if (!values) { + ;(Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (fieldInfo) => { + if ( + fieldInfo.instance && + fieldInfo.instance.options.defaultValue !== undefined + ) { + nextValues = setBy( + nextValues, + fieldInfo.instance.name, + fieldInfo.instance.options.defaultValue, + ) + } + }, + ) + } + + return getDefaultFormState({ ...(this.options.defaultState as any), - values: - values ?? - this.options.defaultValues ?? - this.options.defaultState?.values, + values: nextValues, fieldMetaBase, - }), - ) + }) + }) } /** @@ -2542,15 +2556,21 @@ export class FormApi< */ resetField = >(field: TField) => { this.baseStore.setState((prev) => { + const fieldDefault = this.getFieldInfo(field)?.instance?.options + .defaultValue + const formDefault = getBy(this.options.defaultValues, field) + const targetValue = fieldDefault ?? formDefault + return { ...prev, fieldMetaBase: { ...prev.fieldMetaBase, [field]: defaultFieldMeta, }, - values: this.options.defaultValues - ? setBy(prev.values, field, getBy(this.options.defaultValues, field)) - : prev.values, + values: + targetValue !== undefined + ? setBy(prev.values, field, targetValue) + : prev.values, } }) } diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index abca29868..a4f2714ea 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -120,7 +120,7 @@ describe('field api', () => { expect(field.getMeta().isDefaultValue).toBe(false) field.setValue('test') - expect(field.getMeta().isDefaultValue).toBe(true) + expect(field.getMeta().isDefaultValue).toBe(false) form.resetField('name') expect(field.getMeta().isDefaultValue).toBe(true) @@ -130,6 +130,54 @@ describe('field api', () => { expect(field.getMeta().isDefaultValue).toBe(true) }) + it('should be false when value is undefined and a default value is specified in form-level only', () => { + const form = new FormApi({ + defaultValues: { + name: 'foo', + }, + }) + form.mount() + + const field = new FieldApi({ + form, + name: 'name', + }) + field.mount() + + expect(field.getMeta().isDefaultValue).toBe(true) + + // Set to undefined - should be false because 'foo' is the default + field.setValue(undefined as any) + expect(field.getMeta().isDefaultValue).toBe(false) + }) + + it('should handle falsy values correctly in isDefaultValue', () => { + const form = new FormApi({ + defaultValues: { + count: 0, + active: false, + text: '', + }, + }) + form.mount() + + const countField = new FieldApi({ form, name: 'count' }) + const activeField = new FieldApi({ form, name: 'active' }) + const textField = new FieldApi({ form, name: 'text' }) + countField.mount() + activeField.mount() + textField.mount() + + expect(countField.getMeta().isDefaultValue).toBe(true) + expect(activeField.getMeta().isDefaultValue).toBe(true) + expect(textField.getMeta().isDefaultValue).toBe(true) + + countField.setValue(1) + expect(countField.getMeta().isDefaultValue).toBe(false) + countField.setValue(0) + expect(countField.getMeta().isDefaultValue).toBe(true) + }) + it('should update the fields meta isDefaultValue with arrays - simple', () => { const form = new FormApi({ defaultValues: { From 75dbab5adc1d02c63763450e4689be338abf8797 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:08:18 +0000 Subject: [PATCH 2/5] ci: apply automated fixes and generate docs --- packages/form-core/src/FormApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 702501e9f..c6bf00aad 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2556,8 +2556,8 @@ export class FormApi< */ resetField = >(field: TField) => { this.baseStore.setState((prev) => { - const fieldDefault = this.getFieldInfo(field)?.instance?.options - .defaultValue + const fieldDefault = + this.getFieldInfo(field)?.instance?.options.defaultValue const formDefault = getBy(this.options.defaultValues, field) const targetValue = fieldDefault ?? formDefault From 9dead10e9fc2e5a730f05a2637b32188c2348332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EA=B7=9C=EC=A7=84?= Date: Mon, 26 Jan 2026 01:29:14 +0900 Subject: [PATCH 3/5] chore(form-core): suppress eslint false positives for optional chaining --- packages/form-core/src/FormApi.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index c6bf00aad..f4b6f4d63 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1093,6 +1093,7 @@ export class FormApi< const isFieldPristine = !currBaseMeta.isDirty const isDefaultValue = evaluate( curFieldVal, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.getFieldInfo(fieldName)?.instance?.options.defaultValue ?? getBy(this.options.defaultValues, fieldName), ) @@ -2557,6 +2558,7 @@ export class FormApi< resetField = >(field: TField) => { this.baseStore.setState((prev) => { const fieldDefault = + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.getFieldInfo(field)?.instance?.options.defaultValue const formDefault = getBy(this.options.defaultValues, field) const targetValue = fieldDefault ?? formDefault From 53da788fe37e46961e150623d8558791308e8e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EA=B7=9C=EC=A7=84?= Date: Mon, 26 Jan 2026 01:48:42 +0900 Subject: [PATCH 4/5] test(form-core): add coverage for field-level defaultValue priority in reset --- packages/form-core/tests/FormApi.spec.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index dd418d727..8438c4705 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -169,6 +169,46 @@ describe('form api', () => { expect(form.state.values).toEqual({ name: 'initial' }) }) + it('should prioritize field-level defaultValue over form-level defaultValues on reset', () => { + const form = new FormApi({ + defaultValues: { + name: 'form-default', + age: 25, + }, + }) + form.mount() + + const nameField = new FieldApi({ + form, + name: 'name', + defaultValue: 'field-default', + }) + nameField.mount() + + const ageField = new FieldApi({ + form, + name: 'age', + }) + ageField.mount() + + // Change values + nameField.setValue('changed-name') + ageField.setValue(30) + + expect(form.state.values).toEqual({ + name: 'changed-name', + age: 30, + }) + + // Reset without arguments - field-level defaultValue should take priority + form.reset() + + expect(form.state.values).toEqual({ + name: 'field-default', // field's defaultValue, not form's + age: 25, // form's defaultValues (no field-level default) + }) + }) + it('should handle multiple fields with mixed mount states', () => { const form = new FormApi({ defaultValues: { From 699134cd69f0819b1b27d0d10567aa5d60049fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EA=B7=9C=EC=A7=84?= Date: Wed, 28 Jan 2026 19:44:02 +0900 Subject: [PATCH 5/5] refactor(form-core): remove unnecessary eslint-disable for optional chain --- packages/form-core/src/FormApi.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index f4b6f4d63..32c624fee 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2558,8 +2558,7 @@ export class FormApi< resetField = >(field: TField) => { this.baseStore.setState((prev) => { const fieldDefault = - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.getFieldInfo(field)?.instance?.options.defaultValue + this.getFieldInfo(field).instance?.options.defaultValue const formDefault = getBy(this.options.defaultValues, field) const targetValue = fieldDefault ?? formDefault