Skip to content

Commit 4567071

Browse files
committed
fix(form-core): unify prioritized default logic for isDefaultValue and reset
1 parent 38f2e5d commit 4567071

3 files changed

Lines changed: 94 additions & 21 deletions

File tree

.changeset/gentle-jars-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/form-core': minor
3+
---
4+
5+
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.

packages/form-core/src/FormApi.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,16 +1091,11 @@ export class FormApi<
10911091
// As primitives, we don't need to aggressively persist the same referential value for performance reasons
10921092
const isFieldValid = !isNonEmptyArray(fieldErrors)
10931093
const isFieldPristine = !currBaseMeta.isDirty
1094-
const isDefaultValue =
1095-
evaluate(
1096-
curFieldVal,
1094+
const isDefaultValue = evaluate(
1095+
curFieldVal,
1096+
this.getFieldInfo(fieldName)?.instance?.options.defaultValue ??
10971097
getBy(this.options.defaultValues, fieldName),
1098-
) ||
1099-
evaluate(
1100-
curFieldVal,
1101-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1102-
this.getFieldInfo(fieldName)?.instance?.options.defaultValue,
1103-
)
1098+
)
11041099

11051100
if (
11061101
prevFieldInfo &&
@@ -1507,16 +1502,35 @@ export class FormApi<
15071502
}
15081503
}
15091504

1510-
this.baseStore.setState(() =>
1511-
getDefaultFormState({
1505+
this.baseStore.setState(() => {
1506+
let nextValues =
1507+
values ??
1508+
this.options.defaultValues ??
1509+
this.options.defaultState?.values
1510+
1511+
if (!values) {
1512+
;(Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
1513+
(fieldInfo) => {
1514+
if (
1515+
fieldInfo.instance &&
1516+
fieldInfo.instance.options.defaultValue !== undefined
1517+
) {
1518+
nextValues = setBy(
1519+
nextValues,
1520+
fieldInfo.instance.name,
1521+
fieldInfo.instance.options.defaultValue,
1522+
)
1523+
}
1524+
},
1525+
)
1526+
}
1527+
1528+
return getDefaultFormState({
15121529
...(this.options.defaultState as any),
1513-
values:
1514-
values ??
1515-
this.options.defaultValues ??
1516-
this.options.defaultState?.values,
1530+
values: nextValues,
15171531
fieldMetaBase,
1518-
}),
1519-
)
1532+
})
1533+
})
15201534
}
15211535

15221536
/**
@@ -2542,15 +2556,21 @@ export class FormApi<
25422556
*/
25432557
resetField = <TField extends DeepKeys<TFormData>>(field: TField) => {
25442558
this.baseStore.setState((prev) => {
2559+
const fieldDefault = this.getFieldInfo(field)?.instance?.options
2560+
.defaultValue
2561+
const formDefault = getBy(this.options.defaultValues, field)
2562+
const targetValue = fieldDefault ?? formDefault
2563+
25452564
return {
25462565
...prev,
25472566
fieldMetaBase: {
25482567
...prev.fieldMetaBase,
25492568
[field]: defaultFieldMeta,
25502569
},
2551-
values: this.options.defaultValues
2552-
? setBy(prev.values, field, getBy(this.options.defaultValues, field))
2553-
: prev.values,
2570+
values:
2571+
targetValue !== undefined
2572+
? setBy(prev.values, field, targetValue)
2573+
: prev.values,
25542574
}
25552575
})
25562576
}

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe('field api', () => {
120120
expect(field.getMeta().isDefaultValue).toBe(false)
121121

122122
field.setValue('test')
123-
expect(field.getMeta().isDefaultValue).toBe(true)
123+
expect(field.getMeta().isDefaultValue).toBe(false)
124124

125125
form.resetField('name')
126126
expect(field.getMeta().isDefaultValue).toBe(true)
@@ -130,6 +130,54 @@ describe('field api', () => {
130130
expect(field.getMeta().isDefaultValue).toBe(true)
131131
})
132132

133+
it('should be false when value is undefined and a default value is specified in form-level only', () => {
134+
const form = new FormApi({
135+
defaultValues: {
136+
name: 'foo',
137+
},
138+
})
139+
form.mount()
140+
141+
const field = new FieldApi({
142+
form,
143+
name: 'name',
144+
})
145+
field.mount()
146+
147+
expect(field.getMeta().isDefaultValue).toBe(true)
148+
149+
// Set to undefined - should be false because 'foo' is the default
150+
field.setValue(undefined as any)
151+
expect(field.getMeta().isDefaultValue).toBe(false)
152+
})
153+
154+
it('should handle falsy values correctly in isDefaultValue', () => {
155+
const form = new FormApi({
156+
defaultValues: {
157+
count: 0,
158+
active: false,
159+
text: '',
160+
},
161+
})
162+
form.mount()
163+
164+
const countField = new FieldApi({ form, name: 'count' })
165+
const activeField = new FieldApi({ form, name: 'active' })
166+
const textField = new FieldApi({ form, name: 'text' })
167+
countField.mount()
168+
activeField.mount()
169+
textField.mount()
170+
171+
expect(countField.getMeta().isDefaultValue).toBe(true)
172+
expect(activeField.getMeta().isDefaultValue).toBe(true)
173+
expect(textField.getMeta().isDefaultValue).toBe(true)
174+
175+
countField.setValue(1)
176+
expect(countField.getMeta().isDefaultValue).toBe(false)
177+
countField.setValue(0)
178+
expect(countField.getMeta().isDefaultValue).toBe(true)
179+
})
180+
133181
it('should update the fields meta isDefaultValue with arrays - simple', () => {
134182
const form = new FormApi({
135183
defaultValues: {

0 commit comments

Comments
 (0)