Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/async-default-untouched-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

fix(form-core): apply updated async `defaultValues` to untouched fields even after another field has been edited, instead of skipping the whole update once any field is touched (#2229)
24 changes: 21 additions & 3 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1754,15 +1754,33 @@ export class FormApi<

const shouldUpdateValues =
options.defaultValues &&
!evaluate(options.defaultValues, oldOptions.defaultValues) &&
!this.state.isTouched
!evaluate(options.defaultValues, oldOptions.defaultValues)

const shouldUpdateState =
!evaluate(options.defaultState, oldOptions.defaultState) &&
!this.state.isTouched

if (!shouldUpdateValues && !shouldUpdateState) return

// Fields the user has already edited must keep their edited values, but
// untouched fields (including ones that have not mounted yet) should still
// receive their newly-arrived default values — even when a *different*
// field has already been touched. See #2229.
let nextValues = options.defaultValues
if (shouldUpdateValues && this.state.isTouched) {
for (const [fieldName, fieldMeta] of Object.entries(
this.state.fieldMeta,
)) {
if ((fieldMeta as AnyFieldLikeMeta | undefined)?.isTouched) {
nextValues = setBy(
nextValues,
fieldName,
getBy(this.state.values, fieldName),
)
}
}
}

batch(() => {
this.baseStore.setState(() =>
getDefaultFormState(
Expand All @@ -1774,7 +1792,7 @@ export class FormApi<

shouldUpdateValues
? {
values: options.defaultValues,
values: nextValues,
}
: {},
),
Expand Down
48 changes: 48 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,54 @@ describe('form api', () => {
expect(form.getFieldValue('name')).toEqual('two')
})

it('should apply new default values to untouched fields after another field is edited', () => {
const form = new FormApi({
defaultValues: {
name: '',
asyncField: '',
},
})
form.mount()

// The user edits one field while async data for another is still loading.
form.setFieldValue('name', 'edited')

// The async default for `asyncField` resolves and is passed in via update().
form.update({
defaultValues: {
name: '',
asyncField: 'loaded default',
},
})

// The untouched field receives its newly-arrived default...
expect(form.getFieldValue('asyncField')).toEqual('loaded default')
// ...while the edited field keeps the user's value.
expect(form.getFieldValue('name')).toEqual('edited')
})

it('should apply new default values to untouched array fields after another field is edited', () => {
const form = new FormApi({
defaultValues: {
name: '',
asyncItems: [] as string[],
},
})
form.mount()

form.setFieldValue('name', 'edited')

form.update({
defaultValues: {
name: '',
asyncItems: ['a', 'b'],
},
})

expect(form.getFieldValue('asyncItems')).toEqual(['a', 'b'])
expect(form.getFieldValue('name')).toEqual('edited')
})

it('should delete field from the form', () => {
const form = new FormApi({
defaultValues: {
Expand Down