Skip to content

fix(form-core): treat non-plain objects with no own enumerable keys as unequal in evaluate#2140

Open
Zelys-DFKH wants to merge 1 commit intoTanStack:mainfrom
Zelys-DFKH:fix/evaluate-getter-only-objects
Open

fix(form-core): treat non-plain objects with no own enumerable keys as unequal in evaluate#2140
Zelys-DFKH wants to merge 1 commit intoTanStack:mainfrom
Zelys-DFKH:fix/evaluate-getter-only-objects

Conversation

@Zelys-DFKH
Copy link
Copy Markdown

@Zelys-DFKH Zelys-DFKH commented Apr 30, 2026

🎯 Changes

Fixes #1628.

evaluate() compares objects by iterating Object.keys(). That works fine for plain objects, but for anything whose state lives only in getters, Object.keys() returns []. The key-iteration loop then vacuously succeeds and two distinct instances are treated as equal, regardless of what they actually contain. Temporal.Duration, RegExp, any class that exposes values through getters — they all hit this. The result is that form updates get dropped silently when a field value changes to a new instance of such a type.

The fix is a single guard placed after the key-count check:

if (
  keysA.length === 0 &&
  !Array.isArray(objA) &&
  !Array.isArray(objB) &&
  (Object.getPrototypeOf(objA) !== Object.prototype ||
    Object.getPrototypeOf(objB) !== Object.prototype)
) {
  return false
}

When both objects have zero own enumerable keys and at least one isn't a plain {}, they fall back to referential inequality (Object.is at the top already returned false). Arrays get an explicit carve-out so evaluate([], []) still returns true.

PR #1939 adds a specific instanceof Blob guard for File/Blob. This guard is more general and covers the same case, plus Temporal, RegExp, Error, and anything else with a getter-only design. If #1939 merges first, its instanceof Blob check fires before reaching this one, so there's no conflict. If this merges first, File/Blob is already covered.

One thing to flag: two RegExp literals with identical source and flags (like /abc/g vs /abc/g) now return false, since they're different instances. Previously they returned true vacuously, which was also wrong: it suppressed updates when a regex field actually changed. The behavior is now in the right direction. A dedicated instanceof RegExp handler (like the existing Date one) could add semantic equality if that turns out to be useful.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed form validation comparison logic that was incorrectly treating distinct non-plain objects as equivalent. This affected special object types including Temporal, RegExp, and classes with getter-only properties.
    • Strengthened test coverage to ensure accurate object comparison behavior across all supported object types.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70aa1d66-a278-41d6-b7eb-9a24ebc591e9

📥 Commits

Reviewing files that changed from the base of the PR and between 93be6f0 and 1b02d59.

📒 Files selected for processing (3)
  • .changeset/fix-evaluate-getter-only-objects.md
  • packages/form-core/src/utils.ts
  • packages/form-core/tests/utils.spec.ts

📝 Walkthrough

Walkthrough

This patch fixes the evaluate() function in @tanstack/form-core to correctly distinguish non-plain objects with zero own enumerable keys (such as Temporal types, RegExp, Date, or getter-only class instances) by returning false instead of treating them as equal, preventing silent form option update failures.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changeset/fix-evaluate-getter-only-objects.md
Declares patch version bump for @tanstack/form-core documenting the fix for evaluate() incorrectly treating distinct non-plain objects as equal.
Core Comparison Logic
packages/form-core/src/utils.ts
Adds a guard check to evaluate() that returns false when both inputs are non-array objects with no own enumerable keys and non-Object.prototype prototypes, preventing false positives from key iteration never executing.
Test Coverage
packages/form-core/tests/utils.spec.ts
Adds comprehensive test assertions validating that distinct non-plain objects (getter-based instances, Temporal types, RegExp) evaluate as unequal, while plain empty objects remain equal, and cross-type comparisons behave correctly.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A guard to catch the sneaky trick,
When objects hide with no keys quick,
Now Temporal and Date alike,
Won't fool our form—equality struck right! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: treating non-plain objects with no own enumerable keys as unequal in the evaluate function.
Description check ✅ Passed The description is comprehensive, addressing the root cause, the fix details, interaction with other PRs, and includes all required checklist items marked complete.
Linked Issues check ✅ Passed The PR successfully addresses issue #1628 by implementing the required guard to prevent distinct non-plain objects with no enumerable keys from being treated as equal.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the evaluate() function logic for non-plain objects, with corresponding tests and changeset documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

evaluate does not correctly compare objects which breaks formOptions updates

1 participant