Skip to content

fix(form-core): apply async defaultValues to untouched fields after other fields are edited - #2246

Open
xianjianlf2 wants to merge 1 commit into
TanStack:mainfrom
xianjianlf2:fix/async-default-untouched-fields-2229
Open

fix(form-core): apply async defaultValues to untouched fields after other fields are edited#2246
xianjianlf2 wants to merge 1 commit into
TanStack:mainfrom
xianjianlf2:fix/async-default-untouched-fields-2229

Conversation

@xianjianlf2

@xianjianlf2 xianjianlf2 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Fixes #2229. FormApi.update() previously gated default-value merging on the form-level touched state, so touching one field prevented asynchronously arriving defaults from applying to every other untouched field.

  • Drop the form-level touched gate and merge updated defaults per field instead.
  • Preserve user-edited values for touched fields.
  • Apply new default values to untouched fields, including fields that have not mounted yet.
  • Add regression coverage for both scalar fields and array fields.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.

Notes

The existing behavior of not wiping an edited field during update is preserved. The new behavior only applies newly arrived defaults to fields that are still untouched.

…dits

FormApi.update() gated the whole defaultValues merge on the form-level
`isTouched` flag, which is true as soon as any single field is edited.
As a result, once the user touched one field, asynchronously-arriving
default values were no longer applied to *any* field, leaving untouched
fields empty.

Drop the form-level gate and instead preserve values per-field: edited
(touched) fields keep the user's input, while untouched fields — including
ones that have not mounted yet — receive their newly-arrived defaults.

Closes TanStack#2229
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

FormApi.update now merges incoming default values with current values, preserving touched fields while updating untouched fields. Tests cover scalar and array defaults, and a patch changeset documents the fix.

Changes

Async default value updates

Layer / File(s) Summary
Preserve edits during default updates
packages/form-core/src/FormApi.ts, .changeset/async-default-untouched-fields.md
FormApi.update applies changed defaults to untouched fields while retaining current values for touched fields.
Validate scalar and array defaults
packages/form-core/tests/FormApi.spec.ts
Tests verify that scalar and array defaults update correctly without overwriting edited values.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • TanStack/form#2190: Both changes address async defaultValues updates, including array-field update behavior.

Suggested reviewers: crutchcorn

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code and tests satisfy #2229 by merging async defaults per field and preserving edited values.
Out of Scope Changes check ✅ Passed The changes stay within the issue scope: implementation fix, regression tests, and a changeset entry.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly matches the main change: async defaultValues now apply to untouched fields after edits.
Description check ✅ Passed The description follows the template and includes changes, checklist, release impact, and a changeset note.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MILLERMARRU MILLERMARRU left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked setBy/getBy in utils.ts to confirm the merge approach actually does what the changeset claims. setBy rebuilds the object path-segment by path-segment and only touches the exact path you give it, everything else comes from whatever you passed as the base object. So starting nextValues from the full incoming options.defaultValues and then overwriting just the touched field paths with their current live values via getBy(this.state.values, fieldName) is exactly the right shape for "untouched fields get the new default, touched fields keep what the user typed," and it composes fine for nested/array paths since setBy doesn't care about depth.

The loop iterates this.state.fieldMeta, which only has entries for fields that have actually mounted or been interacted with, so a field that hasn't mounted yet simply has no entry and falls through to the new default untouched, which matches the changeset's claim about fields that "have not mounted yet." That's a detail that's easy to get wrong (checking some kind of "isMounted" flag instead of just relying on absence from the map), and this uses the simpler and correct one.

Good that the second test specifically covers an array field (asyncItems: [] to ['a', 'b']) rather than just a scalar, since arrays are exactly the shape most likely to be loaded asynchronously in a real form and where "touched" tracking has more edge cases than a single string field.

One thing worth a second look, not a blocker: this only preserves values for fields with an existing fieldMeta entry marked isTouched. If a field gets marked touched through something other than direct user edits (an array insert/move marking a parent array field touched as a side effect, for instance) rather than the field's own value being edited, this would also preserve that field's current value over the incoming default. That seems like the right call for an array a user has already manipulated, but wanted to flag it since it's the one place where "touched" doing double duty for both "edited" and "structurally mutated" could matter for less obvious field shapes than the ones in the tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async default values not applied to untouched fields after other fields are edited

2 participants