Skip to content

fix(form-core): reset stale validation counter when shifting array meta - #2247

Open
xianjianlf2 wants to merge 3 commits into
TanStack:mainfrom
xianjianlf2:fix/array-removevalue-isvalidating-stuck-2234
Open

fix(form-core): reset stale validation counter when shifting array meta#2247
xianjianlf2 wants to merge 3 commits into
TanStack:mainfrom
xianjianlf2:fix/array-removevalue-isvalidating-stuck-2234

Conversation

@xianjianlf2

@xianjianlf2 xianjianlf2 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Fixes #2234. When an array item is removed (or inserted/moved) while an async validator is in flight, shiftMeta copied the source field's transient _pendingValidationsCount/isValidating down to the shifted index. The pending promise is owned by the original field instance and settles its counter against the original index, so the shifted index's counter is orphaned and never decremented — leaving form.state.isFieldsValidating stuck true and blocking submission.

  • shiftMeta now resets the transient validation state (isValidating, _pendingValidationsCount) when shifting meta between indices. Array mutations already re-trigger validation on the shifted fields, so this is safe.
  • endValidation bails out if the field's instance is no longer registered, so a validation that resolves after its field was removed no longer resurrects deleted meta (previously threw a TypeError).

✅ 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.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where async validation state could remain stuck after removing an item from an array field during in-flight validation.
    • Ensured transient async-validation indicators are properly reset or preserved when array field metadata shifts due to move, swap, or shift operations.
  • Tests
    • Added coverage for array remove/move/swap scenarios with in-flight async validation, verifying field-level and form-level validating status behavior.

When an array item is removed (or inserted/moved) while an async validator
is in flight, `shiftMeta` copied the source field's transient
`_pendingValidationsCount`/`isValidating` down to the shifted index. The
pending promise is owned by the original field instance and settles its
counter against the original index, so the shifted index's counter is
orphaned and never decremented, leaving `isFieldsValidating` stuck `true`.

Reset the transient validation state when shifting meta, and guard
`endValidation` so a validation that resolves after its field was removed
no longer resurrects deleted meta.

Closes TanStack#2234
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6b3d838-b1f1-4f66-b29f-519d159a421f

📥 Commits

Reviewing files that changed from the base of the PR and between 8488fc3 and 860ab18.

📒 Files selected for processing (2)
  • packages/form-core/src/metaHelper.ts
  • packages/form-core/tests/FieldApi.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/form-core/src/metaHelper.ts

📝 Walkthrough

Walkthrough

This change prevents stale async-validation state after array values are removed or reordered by guarding inactive field validation completion and resetting transient metadata during shifts. Tests cover removal, move, and swap scenarios, with a patch Changesets entry.

Changes

Array async-validation cleanup

Layer / File(s) Summary
Validation lifecycle and metadata shifting
packages/form-core/src/FieldApi.ts, packages/form-core/src/metaHelper.ts
Inactive field instances no longer update metadata after async validation completes, and moved, swapped, or shifted array fields reset isValidating and _pendingValidationsCount.
Regression coverage and release metadata
packages/form-core/tests/FieldApi.spec.ts, .changeset/fix-array-removevalue-isvalidating-stuck.md
Tests cover removal, move, and swap during pending async validation; a patch release entry documents the fix.

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

Possibly related PRs

  • TanStack/form#2143 — Also updates async-validation cleanup and isValidating resolution in FieldApi.ts.

Suggested reviewers: crutchcorn, crutchcorn

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: resetting stale validation state for array meta during form-core updates.
Description check ✅ Passed The description includes the required Changes, Checklist, and Release Impact sections and covers the main fix and testing status.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/form-core/src/metaHelper.ts (1)

100-116: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset async-validation state in handleArrayMove/handleArraySwap. Reapplying fromMeta and swapping meta verbatim carries isValidating and _pendingValidationsCount to the new index, so an in-flight validation can stay stuck on the wrong field. Mirror shiftMeta’s reset here and add a move/swap regression test.

🤖 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/metaHelper.ts` around lines 100 - 116, Reset
async-validation state when restoring metadata in handleArrayMove and
handleArraySwap, matching shiftMeta’s reset behavior instead of copying
isValidating and _pendingValidationsCount unchanged. Update the metadata
transfer logic around fromMeta and swapped metadata, then add regression
coverage verifying moved and swapped fields do not retain in-flight validation
state.
🤖 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.

Outside diff comments:
In `@packages/form-core/src/metaHelper.ts`:
- Around line 100-116: Reset async-validation state when restoring metadata in
handleArrayMove and handleArraySwap, matching shiftMeta’s reset behavior instead
of copying isValidating and _pendingValidationsCount unchanged. Update the
metadata transfer logic around fromMeta and swapped metadata, then add
regression coverage verifying moved and swapped fields do not retain in-flight
validation state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48e8bfdf-c589-40d8-979e-008a9b920059

📥 Commits

Reviewing files that changed from the base of the PR and between 5d11281 and 351d752.

📒 Files selected for processing (4)
  • .changeset/fix-array-removevalue-isvalidating-stuck.md
  • packages/form-core/src/FieldApi.ts
  • packages/form-core/src/metaHelper.ts
  • packages/form-core/tests/FieldApi.spec.ts

@xianjianlf2

xianjianlf2 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit follow-up in 8488fc3. Move and swap now use the same transient async-validation reset as shifted array meta, so isValidating / _pendingValidationsCount are not copied to a different index. Added regression coverage for both moveValue and swapValues.

Local checks:

  • PATH=/Users/even/.nvm/versions/node/v24.18.0/bin:$PATH pnpm --dir packages/form-core test:lib tests/FieldApi.spec.ts
  • PATH=/Users/even/.nvm/versions/node/v24.18.0/bin:$PATH pnpm --dir packages/form-core test:types:ts59

@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/metaHelper.ts`:
- Around line 113-116: Preserve validation metadata for no-op operations by
returning early when fromIndex === toIndex in moveValue at
packages/form-core/src/metaHelper.ts:113-116, and when index === secondIndex in
swapValues at packages/form-core/src/metaHelper.ts:159-162, before resetting
field metadata. Add regression coverage for same-index moveValue and swapValues
calls with dontValidate enabled to verify pending validation remains intact.
🪄 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: 406bd3eb-5e14-4388-a433-7a8336813edd

📥 Commits

Reviewing files that changed from the base of the PR and between 351d752 and 8488fc3.

📒 Files selected for processing (2)
  • packages/form-core/src/metaHelper.ts
  • packages/form-core/tests/FieldApi.spec.ts

Comment thread packages/form-core/src/metaHelper.ts
@xianjianlf2

Copy link
Copy Markdown
Contributor Author

Addressed the same-index move/swap follow-up in 860ab18.

What changed:

  • moveValue(i, i, { dontValidate: true }) now returns after bumping the array version, before meta shifting/resetting.
  • swapValues(i, i, { dontValidate: true }) does the same.
  • Added regression coverage to verify isValidating and _pendingValidationsCount remain intact for both no-op operations.

Local check:

pnpm --filter @tanstack/form-core exec vitest run tests/FieldApi.spec.ts

Result: 1 file / 110 tests passed, no type errors.

@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 how this.getInfo().instance !== this is used elsewhere in FieldApi.ts before judging this guard, and it's not a new idea being introduced here, the async validator resolution path around line 1554 already bails out of writing setMeta with that exact same check when a field's identity has been superseded. So endValidation() picking up the same guard is consistent with how the file already treats "this instance no longer owns this name" elsewhere, rather than a one-off patch.

What makes this actually close the bug rather than just move it is that endValidation() runs unconditionally after Promise.all(validatesPromises) in validateAsync, regardless of that existing instance check further up (which only guards the error-writing setMeta, not the counter decrement). So before this fix, even in the path that correctly skipped writing a stale error, the pending-validation counter on whatever now occupies that field name would still get decremented by an instance that no longer owns it, which is exactly how isFieldsValidating could end up permanently wrong after removeValue shifts indices.

The resetTransientValidationMeta addition in metaHelper.ts is the other half of the same problem: when array mutation copies old meta onto a new key, carrying over isValidating/_pendingValidationsCount would leave a counter at the new key that nothing will ever decrement, since the instance that started that validation is bailing out via the guard above instead of settling it. Resetting it at copy time and letting the shift naturally re-trigger validation on the moved field is the right call.

The fromIndex === toIndex / index === secondIndex early returns are a nice touch beyond the core fix, and the test for the same-index case (should preserve transient async validation state when moving array field meta to the same index) is what actually proves the reset only fires on a real move and not on a no-op call, which is easy to get backwards.

The main regression test reconstructs the real scenario end to end, an async validator on a listened-to field, in flight, then removing a different array index while it's pending, then resolving it and checking the counter settles at zero rather than asserting some internal method got called. That's the right level to test this at since the bug only shows up through that exact interleaving.

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.

Using onChangeAsync in an array field causes isFieldsValidating to always be true after calling removeValue

2 participants