fix(form-core): prevent removeValue from auto-touching shifted siblings#2133
fix(form-core): prevent removeValue from auto-touching shifted siblings#2133mixelburg wants to merge 1 commit intoTanStack:mainfrom
Conversation
When calling form.removeValue(index) on an array field, isTouched was incorrectly set to true on every sibling at index >= removed index, even when the user never interacted with those fields. The fix bypasses validateField for fields with existing instances and calls fieldInstance.validate() directly, which preserves validation without the auto-touch side-effect. Fixes TanStack#2131
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/form-core/src/FormApi.ts`:
- Around line 1608-1616: The current code calls fieldInstance.validate(cause)
which avoids auto-touching but relies on FieldApi.validate returning [] when
meta.isTouched is false, leaving shifted untouched siblings unvalidated; change
this by adding an internal "validate without touching" path: extend
validateField to accept an options flag (e.g., { touch?: boolean }) or add a new
internal method (e.g., validateFieldInternal/FieldApi.validateWithoutTouch) that
runs the same validation logic regardless of meta.isTouched but does not mutate
touch state, then replace the direct call to fieldInstance.validate(cause) with
a call to validateField(nestedField, cause, { touch: false }) or the new
internal method so shifted siblings revalidate without auto-touch side effects;
update FieldApi.validate to delegate to the shared internal validator to avoid
duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d506b7d4-0546-43d1-a8b3-a5847867e52b
📒 Files selected for processing (1)
packages/form-core/src/FormApi.ts
| Promise.resolve().then(() => { | ||
| // If the field instance already exists, call validate directly | ||
| // to avoid auto-touching shifted siblings | ||
| const fieldInstance = this.fieldInfo[nestedField]?.instance | ||
| if (fieldInstance) { | ||
| return fieldInstance.validate(cause) | ||
| } | ||
| return this.validateField(nestedField, cause) | ||
| }), |
There was a problem hiding this comment.
Preserve validation when bypassing auto-touch.
Calling fieldInstance.validate(cause) directly skips validation for untouched fields because FieldApi.validate returns [] when meta.isTouched is false (packages/form-core/src/FieldApi.ts:1991-1997). This fixes the touched mutation, but shifted untouched siblings will no longer revalidate, leaving stale field errors possible after insert/remove/replace.
Consider adding an internal “validate without touching” path instead of calling FieldApi.validate as-is.
Possible direction
- if (fieldInstance) {
- return fieldInstance.validate(cause)
- }
- return this.validateField(nestedField, cause)
+ return this.validateField(nestedField, cause, {
+ touch: false,
+ })This would require extending validateField to make the isTouched mutation optional while still running the same validation logic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/form-core/src/FormApi.ts` around lines 1608 - 1616, The current code
calls fieldInstance.validate(cause) which avoids auto-touching but relies on
FieldApi.validate returning [] when meta.isTouched is false, leaving shifted
untouched siblings unvalidated; change this by adding an internal "validate
without touching" path: extend validateField to accept an options flag (e.g., {
touch?: boolean }) or add a new internal method (e.g.,
validateFieldInternal/FieldApi.validateWithoutTouch) that runs the same
validation logic regardless of meta.isTouched but does not mutate touch state,
then replace the direct call to fieldInstance.validate(cause) with a call to
validateField(nestedField, cause, { touch: false }) or the new internal method
so shifted siblings revalidate without auto-touch side effects; update
FieldApi.validate to delegate to the shared internal validator to avoid
duplication.
What changed
In
validateArrayFieldsStartingFrom, fields with an existing instance now callfieldInstance.validate(cause)directly instead of going throughthis.validateField(). This avoids the auto-touch side-effect invalidateFieldthat marks fields as touched even when the user never interacted with them.Why
Calling
form.removeValue(index)on an array field was flippingisTouchedtotrueon every sibling at index ≥ removed index. The bug lives invalidateFieldwhich auto-touches any field that has an instance but isn't yet touched.How to reproduce
form.removeValue(0)without interacting with other fieldsisTouchedbecomestrueon all shifted siblingsCloses #2131
Summary by CodeRabbit
Bug Fixes