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.
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
❌ Failed | 1m 26s | View ↗ |
nx run-many --target=build --exclude=examples/** |
✅ Succeeded | 34s | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-04-20 05:38:36 UTC

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