From 8c3327c05acf1d984386fb7bf8d98395e38e16ba Mon Sep 17 00:00:00 2001 From: Mariana Castro Date: Fri, 9 Oct 2026 22:18:37 -0300 Subject: [PATCH 1/2] fix(form-core): reset field to undefined default in resetField resetField skipped the value update whenever the resolved default was undefined, treating "the default is undefined" as "no default exists". The field meta was reset while the field kept its current value. Decide whether a default exists from its sources (a field-level defaultValue or form-level defaultValues) instead of from the resolved value, so optional fields are reset to undefined again. Fixes #2424 --- .changeset/calm-fields-reset.md | 5 ++ packages/form-core/src/FormApi.ts | 9 ++- packages/form-core/tests/FormApi.spec.ts | 96 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 .changeset/calm-fields-reset.md diff --git a/.changeset/calm-fields-reset.md b/.changeset/calm-fields-reset.md new file mode 100644 index 0000000000..b0f3862779 --- /dev/null +++ b/.changeset/calm-fields-reset.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Fix `form.resetField()` leaving the current value in place when the field's default value is `undefined`. The field is now reset to `undefined`, matching `form.reset()` and `isDefaultValue`. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d30..4505e75fe9 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2941,6 +2941,8 @@ export class FormApi< this.getFieldInfo(field).instance?.options.defaultValue const formDefault = getBy(this.options.defaultValues, field) const targetValue = fieldDefault ?? formDefault + const hasDefault = + fieldDefault !== undefined || this.options.defaultValues !== undefined return { ...prev, @@ -2948,10 +2950,9 @@ export class FormApi< ...prev.fieldMetaBase, [field]: defaultFieldMeta, }, - values: - targetValue !== undefined - ? setBy(prev.values, field, targetValue) - : prev.values, + values: hasDefault + ? setBy(prev.values, field, targetValue) + : prev.values, } }) } diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c57bb044e8..0f98509526 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -4236,6 +4236,102 @@ it('should preserve nested fields on resetField if defaultValues is not provided expect(form.state.values.nested.field.name).toEqual('Nested') }) +it.each([ + ['undefined', undefined], + ['null', null], + ['an empty string', ''], + ['false', false], + ['0', 0], + ['a defined value', 'tony'], +])( + 'should reset a field to a form-level default of %s on resetField', + (_, defaultValue) => { + const form = new FormApi({ + defaultValues: { name: defaultValue } as { name: unknown }, + }) + form.mount() + + const field = new FieldApi({ form, name: 'name' }) + field.mount() + + field.setValue('hawk') + expect(field.getMeta().isDefaultValue).toBe(false) + + form.resetField('name') + + expect(form.state.values.name).toBe(defaultValue) + expect(field.state.value).toBe(defaultValue) + expect(field.getMeta().isDefaultValue).toBe(true) + expect(field.getMeta().isTouched).toBe(false) + }, +) + +it('should reset a field to undefined on resetField if its key is missing from defaultValues', () => { + const form = new FormApi({ + defaultValues: {} as { name?: string }, + }) + form.mount() + + const field = new FieldApi({ form, name: 'name' }) + field.mount() + + field.setValue('hawk') + form.resetField('name') + + expect(form.state.values.name).toBeUndefined() + expect(field.getMeta().isDefaultValue).toBe(true) +}) + +it('should reset nested fields with undefined defaults on resetField without changing their siblings', () => { + const form = new FormApi({ + defaultValues: { + nested: { name: undefined, age: 1 }, + people: [{ name: undefined }, { name: 'second' }], + } as { + nested: { name?: string; age: number } + people: { name?: string }[] + }, + }) + form.mount() + + form.setFieldValue('nested.name', 'Nested') + form.setFieldValue('nested.age', 2) + form.setFieldValue('people[0].name', 'first') + form.setFieldValue('people[1].name', 'changed') + + form.resetField('nested.name') + form.resetField('people[0].name') + + expect(form.state.values).toEqual({ + nested: { name: undefined, age: 2 }, + people: [{ name: undefined }, { name: 'changed' }], + }) +}) + +it.each([ + ['a defined value', 'tony'], + ['an empty string', ''], + ['false', false], + ['0', 0], +])( + 'should reset a field to a field-level default of %s on resetField if defaultValues is not provided', + (_, defaultValue) => { + const form = new FormApi({ + defaultState: { values: { name: 'initial' } as { name: unknown } }, + }) + form.mount() + + const field = new FieldApi({ form, name: 'name', defaultValue }) + field.mount() + + field.setValue('hawk') + form.resetField('name') + + expect(form.state.values.name).toBe(defaultValue) + expect(field.getMeta().isDefaultValue).toBe(true) + }, +) + it('should accept formId and return it', () => { const form = new FormApi({ defaultValues: { age: 0 }, From 5e4bafdfb68366ef7cb32e2508a3d515301c6f17 Mon Sep 17 00:00:00 2001 From: Mariana Castro Date: Mon, 12 Oct 2026 09:01:09 -0300 Subject: [PATCH 2/2] fix(form-core): restore field-level null default in resetField resetField resolved its target with `fieldDefault ?? formDefault`, so a field-level defaultValue of null fell through to the form-level default. Without defaultValues the field was reset to undefined instead of null, which disagreed with form.reset(). Only fall back to the form-level default when the field-level default is undefined. --- .changeset/calm-fields-reset.md | 2 +- packages/form-core/src/FormApi.ts | 3 ++- packages/form-core/tests/FormApi.spec.ts | 33 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.changeset/calm-fields-reset.md b/.changeset/calm-fields-reset.md index b0f3862779..0b23c13a70 100644 --- a/.changeset/calm-fields-reset.md +++ b/.changeset/calm-fields-reset.md @@ -2,4 +2,4 @@ '@tanstack/form-core': patch --- -Fix `form.resetField()` leaving the current value in place when the field's default value is `undefined`. The field is now reset to `undefined`, matching `form.reset()` and `isDefaultValue`. +Fix `form.resetField()` leaving the current value in place when the field's default value is `undefined`. The field is now reset to `undefined`, matching `form.reset()` and `isDefaultValue`. A field-level `defaultValue` of `null` is also restored instead of falling through to the form-level default. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4505e75fe9..4982a124ff 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2940,7 +2940,8 @@ export class FormApi< const fieldDefault = this.getFieldInfo(field).instance?.options.defaultValue const formDefault = getBy(this.options.defaultValues, field) - const targetValue = fieldDefault ?? formDefault + const targetValue = + fieldDefault !== undefined ? fieldDefault : formDefault const hasDefault = fieldDefault !== undefined || this.options.defaultValues !== undefined diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 0f98509526..448b162dfa 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -4332,6 +4332,39 @@ it.each([ }, ) +it('should reset a field to a field-level default of null on resetField if defaultValues is not provided', () => { + const form = new FormApi({ + defaultState: { values: { name: 'initial' } as { name: unknown } }, + }) + form.mount() + + const field = new FieldApi({ form, name: 'name', defaultValue: null }) + field.mount() + + field.setValue('hawk') + form.resetField('name') + + expect(form.state.values.name).toBeNull() + expect(field.state.value).toBeNull() + expect(field.getMeta().isTouched).toBe(false) +}) + +it('should prefer a field-level default of null over the form-level default on resetField', () => { + const form = new FormApi({ + defaultValues: { name: 'tony' } as { name: unknown }, + }) + form.mount() + + const field = new FieldApi({ form, name: 'name', defaultValue: null }) + field.mount() + + field.setValue('hawk') + form.resetField('name') + + expect(form.state.values.name).toBeNull() + expect(field.state.value).toBeNull() +}) + it('should accept formId and return it', () => { const form = new FormApi({ defaultValues: { age: 0 },