fix(form-core): apply async defaultValues to untouched fields after other fields are edited - #2246
Conversation
…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
📝 WalkthroughWalkthrough
ChangesAsync default value updates
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
MILLERMARRU
left a comment
There was a problem hiding this comment.
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.
🎯 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.✅ Checklist
🚀 Release Impact
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.