Skip to content

fix: track visited pairs in deepEqual to avoid false positives with shared object references#13515

Merged
bluebill1049 merged 1 commit into
react-hook-form:masterfrom
DucMinhNe:fix/deep-equal-shared-references
Jun 10, 2026
Merged

fix: track visited pairs in deepEqual to avoid false positives with shared object references#13515
bluebill1049 merged 1 commit into
react-hook-form:masterfrom
DucMinhNe:fix/deep-equal-shared-references

Conversation

@DucMinhNe

Copy link
Copy Markdown
Contributor

Proposed Changes

deepEqual returns false positives for plain, non-circular structures that reuse an object reference.

const shared = { value: 1 };
deepEqual({ first: shared, second: shared }, { first: { value: 1 }, second: { value: 2 } });
// => true, expected false (second differs: 1 vs 2)

Root cause: the circular-reference guard introduced in #12914 keeps one shared WeakSet of every object ever visited and short-circuits to true when either side has been seen before. The first comparison of shared vs { value: 1 } adds both to the set, so when the traversal reaches second, shared is already "visited" and { value: 2 } is never actually compared. Equality is decided by visit history rather than by structure.

This is user-visible through the values prop: useForm compares incoming values against the previous ones with deepEqual, so a rerender whose values reuse one object reference is silently ignored and the form keeps stale values (repro included as a useForm test).

This PR tracks visited pairs (WeakMap<object, WeakSet<object>>) instead of individual objects, which is the standard approach for cycle detection in structural equality. Termination for circular structures is still guaranteed: each (object1, object2) pair is recorded before recursing and there are finitely many pairs, so any cycle eventually revisits the same pair and short-circuits.

Fixes # (no open issue; the regression was introduced by #12914)

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes

Verified locally: new unit tests + the useForm regression test fail on master and pass with the fix; the existing circular-reference tests from #12914 still pass; jest on deepEqual/useForm/formState/setValue suites → 173 passed; pnpm test:type, pnpm lint, pnpm build (incl. ESM/CJS export assertions) and pnpm api-extractor all pass; dist/index.cjs.js stays within the bundlewatch budget (12.25 kB gzip < 12.5 kB).

…hared object references

The circular-reference guard added in react-hook-form#12914 keeps a single WeakSet of
every object ever visited and returns true as soon as either side has
been seen before. Equality then becomes sticky: once an object has been
compared once, any later comparison involving it short-circuits to
true, so plain non-circular structures that merely reuse an object
reference compare as equal when they are not.

Track visited (object1, object2) pairs in a WeakMap<object, WeakSet>
instead. Termination for circular structures is still guaranteed
because each pair is recorded before recursing and there are finitely
many pairs.

This is user-visible through the values prop: useForm compares the
incoming values against the previous ones with deepEqual, so a
rerender whose values reuse one object reference was silently ignored
and the form kept stale values.
@bluebill1049
bluebill1049 merged commit a2a3c9c into react-hook-form:master Jun 10, 2026
6 checks passed
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.

2 participants