fix(form-core): report isValidating on every async validation run - #2373
fix(form-core): report isValidating on every async validation run#2373ousamabenyounes wants to merge 1 commit into
Conversation
The debounce timer id stored in `timeoutIds.validations` was never released once the timer fired. The next async run read that stale id as "a previous run is still pending" and ran the compensating `endValidation()`, which decremented the pending-validation counter for a run that had already finished. `isValidating` therefore dropped back to false in the same tick the second run started, and stayed false for the rest of that run. Release the id inside the timer callback so the compensation only applies to a run that is genuinely still waiting on its debounce. Fixes TanStack#2372
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe async validation debounce callback now clears its stored timeout ID. A regression test verifies that ChangesAsync validation state
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to Async field validation now reports isValidating during each repeated validation run, with regression coverage for error and recovery behavior. The change is ready to merge. 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 |
🎯 Changes
Fixes #2372 — after the first async field validation completes,
isValidatingnever becomestrueagain, so a "Checking…" indicator only ever shows once.Root cause. In
FieldApi.validateAsync, the debounce timer id is stored intimeoutIds.validations[cause]but is never released once the timer has fired. The branch above it treats a stored id as "a previous run is still pending" and compensates for that run withendValidation():That compensation is correct for a run whose timer is cleared before it fires: such a run's inner promise never settles, so its own final
endValidation()never runs. Applied to an already-fired id it is an unbalanced decrement. The second run doesstartValidation()(_pendingValidationsCount0 → 1,isValidating: true) and then immediately decrements back to 0 in the same tick, soisValidatingreadsfalsefor the whole run.Traced on
main,isValidating/_pendingValidationsCountper store notification:Fix. Release the id inside the timer callback, so the compensation only applies to a run that is genuinely still waiting on its debounce. One statement in
form-core;nullis already the "no pending timer" sentinel used by the unmount cleanup for the same field.One behaviour delta worth flagging
The stale-id branch was also, by accident, the only thing that decremented the counter for a validator that never settles and ignores its
AbortSignal. I measured that case on both refs withonChangeAsync: async () => new Promise(() => {}):mainisValidating: true(count 1)isValidating: true(count 1) — unchangedisValidating: false(count 0)isValidating: true(count 1)So "stuck validating on a validator that never resolves" is already
main's behaviour in the single-run case; this PR does not introduce that mode. The only delta is the superseded case, wheremainreportedfalsepurely via the same unbalanced decrement that causes #2372 for well-behaved validators. Making aborted-but-unsettled runs settle would be a change to form-core's validation accounting well beyond this bug, so I left it alone — glad to follow up if you want it addressed.Also out of scope
FormGroupApiwrites into the sametimeoutIds.validationsslot for related fields and likewise never releases the id, so a group-driven async validation can leave a stale id that a later field-level run then compensates for. That path drivesisValidatingthrough directsetMetacalls and never touches_pendingValidationsCount, so it is a different accounting model, and I did not want to change it without a reproduction I could stand behind. Happy to extend this PR if you would like that path covered too.Test verification (RED → GREEN)
New test:
should set isValidating again on every async validation run after the firstinpackages/form-core/tests/FieldApi.spec.ts. It asserts the first run reportsisValidating(which already works) and then the second run, so it cannot pass by disabling validation — it also asserts the error appears on run 1 and is gone on run 2.RED — new test applied to unmodified
main, no production change:GREEN — same test, unchanged, with the fix:
Full local validation
Ran the full local suite as
pr.ymldoes —nx run-many --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,buildover all 60 projects, thenbuild:all, then aprettier --checkpass — on themainbaseline and on this branch. Both exit 0, with an identical lint-warning signature (0 errors on both).mainbaselineNo failures on either side; the only delta is the one test this PR adds.
✅ Checklist
pnpm test:pr.🚀 Release Impact
Summary by CodeRabbit
Bug Fixes
isValidatingis set totrueduring every validation run, including repeated validations.Tests