Skip to content

fix(form-core): don't auto-touch shifted siblings on array mutation#2225

Open
chatman-media wants to merge 1 commit into
TanStack:mainfrom
chatman-media:fix/array-shift-touched
Open

fix(form-core): don't auto-touch shifted siblings on array mutation#2225
chatman-media wants to merge 1 commit into
TanStack:mainfrom
chatman-media:fix/array-shift-touched

Conversation

@chatman-media

@chatman-media chatman-media commented Jun 26, 2026

Copy link
Copy Markdown

Closes #2131

Problem

form.removeValue(index) (and insertValue/replaceValue) flips isTouched to true on every sibling that shifts, even when the user never interacted with them. Because the bug is in form-core, every adapter is affected. The maintainer confirmed the touch "makes little sense" in #2131.

Cause

After an array mutation, validateArrayFieldsStartingFrom re-validates the shifted items. It routes each one through validateField, which auto-touches the field as a side-effect (so that FieldApi.validate's pristine guard doesn't short-circuit). The shifted siblings genuinely need re-validating — their value changed — but they should not be marked touched.

Fix

A prior attempt (#2133) was closed as overcomplicated; the maintainer hinted the fix should target a different function. This keeps it entirely inside validateArrayFieldsStartingFrom (the function that owns the shift-revalidation), with no new option threading: remember each mounted field's prior isTouched, still call validateField so validation runs, then restore isTouched: false for the fields that weren't touched before. Fields that were touched, and form-level validation for unmounted fields, are unchanged.

Tests

  • New form-core regression test: removing an item from a 5-element array leaves all remaining siblings isTouched: false. Fails without the fix ([false, false, true, true]).
  • Updated two FieldGroupApi forwarding tests that previously asserted the buggy auto-touch. The dedicated one now also asserts the shifted field's validator actually ran (errors === ['validated']), so it still proves forwarding while verifying no spurious touch.

form-core (499) and react-form (123) suites pass; eslint, prettier and type checks (TS 5.4–5.9) clean.

Summary by CodeRabbit

  • Bug Fixes
    • Improved array field validation so shifting items no longer marks untouched fields as touched.
    • Preserved touch state when validating nested fields after removing an array item.
    • Kept validation errors working as expected while preventing unintended touch updates on sibling fields.
  • Tests
    • Updated and added coverage for array field shifting and removal scenarios to verify touch state remains unchanged for untouched fields.

validateArrayFieldsStartingFrom re-validates the array items shifted by
an insert/remove/replace. It went through validateField, which touches
each field as a side-effect, so siblings the user never interacted with
were flipped to isTouched: true.

The shifted fields still need validating (their value changed), so keep
calling validateField, but restore the prior touched state for fields
that weren't touched before the call.

Closes TanStack#2131
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Array field validation now preserves touched state for shifted siblings while still returning validation errors. Regression tests cover forwarded array validation and removeFieldValue to confirm untouched shifted fields stay untouched.

Changes

Array shift touched-state fix

Layer / File(s) Summary
Validation preserves touched state
packages/form-core/src/FormApi.ts
validateArrayFieldsStartingFrom snapshots each shifted field’s touched state, runs validation, and restores untouched mounted fields back to isTouched = false.
Regression tests for shifted fields
packages/form-core/tests/FieldGroupApi.spec.ts, packages/form-core/tests/FormApi.spec.ts
Tests assert shifted array fields receive validation errors without becoming touched across forwarded array validation and removeFieldValue('names', 2) flows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A bunny hopped through array lanes bright,
and kept each shifted sibling just right.
Validation chimed, then softly reset,
so untouched paws stayed untouched yet.
🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the repository template and omits the required checklist and release-impact sections. Add the '🎯 Changes', '✅ Checklist', and '🚀 Release Impact' sections, and fill in the required checkboxes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main behavior change in form-core.
Linked Issues check ✅ Passed The changes address #2131 by keeping array-shifted siblings untouched while still revalidating them.
Out of Scope Changes check ✅ Passed The PR stays focused on the array-mutation touch bug fix and its regression tests; no unrelated changes are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/form-core/src/FormApi.ts`:
- Around line 1920-1937: The shifted-field validation in FormApi still goes
through validateField, which auto-touches mounted fields and can trigger
touch-gated side effects before the flag is restored. Update the validation flow
used for array-shifted fields in FormApi so it bypasses the isTouched mutation
entirely for fields that were not previously touched, while still running
validation and preserving existing mounted-field error behavior. Use the
fieldInstance / wasTouched logic around validateField as the entry point, and
add a regression test covering remove/insert/replace cases to ensure on-mount
errors remain intact without spuriously touching siblings.
🪄 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: 6942c34d-2adf-42d1-99e9-705feb7ee690

📥 Commits

Reviewing files that changed from the base of the PR and between 6a73479 and 7a1314e.

📒 Files selected for processing (3)
  • packages/form-core/src/FormApi.ts
  • packages/form-core/tests/FieldGroupApi.spec.ts
  • packages/form-core/tests/FormApi.spec.ts

Comment on lines +1920 to +1937
Promise.resolve().then(() => {
// These fields were merely shifted by an array mutation, the user
// never interacted with them. Their value did change, so they
// still need to be validated, but `validateField` auto-touches as
// a side-effect — which would spuriously mark untouched siblings
// as touched. Remember the prior touched state and restore it for
// fields that weren't touched before.
const fieldInstance = this.fieldInfo[nestedField]?.instance
const wasTouched = fieldInstance?.store.state.meta.isTouched ?? true

const result = this.validateField(nestedField, cause)

if (fieldInstance && !wasTouched) {
fieldInstance.setMeta((prev) => ({ ...prev, isTouched: false }))
}

return result
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

This still runs the auto-touch side effect through validateField.

Line 1930 still calls validateField, and that method sets isTouched = true for mounted fields before validating on Lines 1967-1971. Restoring false afterward only fixes the final flag; it does not prevent touch-gated side effects that can fire during that window (for example the shouldInvalidateOnMount path later in this file). To fully match the PR objective, shifted mounted fields need a validation path that bypasses the touch mutation entirely, plus a regression test that preserves existing on-mount errors after remove/insert/replace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form-core/src/FormApi.ts` around lines 1920 - 1937, The
shifted-field validation in FormApi still goes through validateField, which
auto-touches mounted fields and can trigger touch-gated side effects before the
flag is restored. Update the validation flow used for array-shifted fields in
FormApi so it bypasses the isTouched mutation entirely for fields that were not
previously touched, while still running validation and preserving existing
mounted-field error behavior. Use the fieldInstance / wasTouched logic around
validateField as the entry point, and add a regression test covering
remove/insert/replace cases to ensure on-mount errors remain intact without
spuriously touching siblings.

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.

removeValue on array field incorrectly sets isTouched=true on all shifted siblings

1 participant