Skip to content

fix(adapters): make shallow equality handle Temporal value objects#272

Open
mfrancis107 wants to merge 3 commits intoTanStack:mainfrom
mfrancis107:michael/temporal-fix
Open

fix(adapters): make shallow equality handle Temporal value objects#272
mfrancis107 wants to merge 3 commits intoTanStack:mainfrom
mfrancis107:michael/temporal-fix

Conversation

@mfrancis107
Copy link

@mfrancis107 mfrancis107 commented Jan 30, 2026

🎯 Changes

  • Fix adapter default shallow equality to avoid treating all “keyless” objects as equal (Temporal objects expose no enumerable keys).
  • Preserve value semantics for value-objects by using .equals() when available (e.g. Temporal).
  • Add Temporal regression tests across adapters (React/Preact/Solid/Vue/Svelte) plus an Angular integration test to ensure Temporal updates trigger re-render.

Why

The adapters default to shallow equality. Previously, shallow could return true for two different object instances if both had zero enumerable keys, because there were no keys to compare. Temporal types from temporal-polyfill are keyless, so updates to Temporal values could be incorrectly treated as “unchanged” and skip UI updates.

Behavioral notes

  • Existing behavior for primitives, Maps, Sets, and Dates is preserved.
  • Plain objects/arrays keep their expected semantics.
  • {} vs [] is now treated as not equal (previously could be considered equal due to both being “keyless”).
  • Keyless non-plain objects are treated as not equal unless:
    • they are the same reference (Object.is), or
    • they implement .equals() (Temporal), in which case .equals() is used.

Consumers can still provide options.equal for custom semantics.

Related issues

Fixes:
TanStack/form#1628
#218

Verification in downstream

  • Verified in local TanStack Form

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

The adapters’ default `shallow` equality treated any two “keyless” objects as
equal (no enumerable keys => no comparisons), which caused Temporal objects to
be considered unchanged and skip UI updates.

Adjust `shallow` so keyless values are only equal for plain objects/arrays, and
use `.equals()` when available (e.g. Temporal) to preserve value semantics.

Add Temporal regression coverage across React/Preact/Solid/Vue/Svelte, plus an
Angular integration test to ensure Temporal updates trigger re-render.
Enhanced the `shallow` equality function across multiple adapters (React, Preact, Solid, Vue, Svelte, Angular) to correctly handle Temporal value objects. Introduced a method to check for Temporal branding using `Symbol.toStringTag`, ensuring accurate comparisons and triggering UI updates when necessary. Added tests for Temporal support in the relevant frameworks.
@mfrancis107 mfrancis107 closed this Feb 2, 2026
@mfrancis107 mfrancis107 reopened this Feb 2, 2026
@mfrancis107
Copy link
Author

Updated PR

Temporal types (native or polyfill) define Symbol.toStringTag values like "Temporal.PlainDate" as part of the TC39 Temporal spec, which makes this check reliable across realms/polyfills (unlike instanceof).

So we can combine check Symbol.ToStringTag starts with Temporal. and that the object has .equals method.

Refactored the `shallow` equality function in Angular, Preact, React, Solid, Svelte, and Vue to improve handling of Temporal objects. The implementation now ensures accurate comparisons by checking `Symbol.toStringTag` for Temporal branding and simplifies the logic for keyless values. Removed redundant checks for plain objects and arrays, enhancing code clarity. Updated tests to reflect these changes and maintain coverage for Temporal support.
@mfrancis107
Copy link
Author

a bit more cleanup. some of the original work wasn't necessary after change to use string tag check.

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.

1 participant