Skip to content

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

Merged
crutchcorn merged 1 commit into
TanStack:mainfrom
Zelys-DFKH:fix/evaluate-getter-only-objects
May 10, 2026
Merged

fix(form-core): treat non-plain objects with no own enumerable keys as unequal in evaluate#2140
crutchcorn merged 1 commit into
TanStack:mainfrom
Zelys-DFKH:fix/evaluate-getter-only-objects

Conversation

@Zelys-DFKH
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Member

@crutchcorn crutchcorn left a comment

Choose a reason for hiding this comment

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

This is great, thanks!

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 10, 2026

View your CI Pipeline Execution ↗ for commit 1b02d59

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 13s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 33s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-10 19:48:52 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 10, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2140

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2140

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2140

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2140

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2140

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2140

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2140

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2140

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2140

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2140

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2140

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2140

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2140

commit: 1b02d59

@sentry
Copy link
Copy Markdown

sentry Bot commented May 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.29%. Comparing base (6892ed0) to head (1b02d59).
⚠️ Report is 200 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2140      +/-   ##
==========================================
- Coverage   90.35%   90.29%   -0.07%     
==========================================
  Files          38       49      +11     
  Lines        1752     2050     +298     
  Branches      444      538      +94     
==========================================
+ Hits         1583     1851     +268     
- Misses        149      179      +30     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@crutchcorn crutchcorn merged commit 5dd1ed4 into TanStack:main May 10, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request May 10, 2026
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

2 participants