Skip to content

fix: useDeepCompareEffect throws on null-prototype objects#2711

Open
chatman-media wants to merge 1 commit into
streamich:masterfrom
chatman-media:fix/deep-compare-null-prototype
Open

fix: useDeepCompareEffect throws on null-prototype objects#2711
chatman-media wants to merge 1 commit into
streamich:masterfrom
chatman-media:fix/deep-compare-null-prototype

Conversation

@chatman-media

Copy link
Copy Markdown

Closes #2700.

Problem

useDeepCompareEffect (and useCustomCompareEffect / useBattery via the shared src/misc/isDeepEqual.ts) throws when a dependency is an object created with Object.create(null):

TypeError: a.valueOf is not a function

isDeepEqual simply re-exported fast-deep-equal/react, which takes a valueOf/toString shortcut:

if (a.valueOf  !== Object.prototype.valueOf)  return a.valueOf()  === b.valueOf();
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();

A null-prototype object has no valueOf, so a.valueOf is undefined; undefined !== Object.prototype.valueOf is true, and the code then calls a.valueOf() and crashes. This is the upstream issue epoberezkin/fast-deep-equal#111, which has been unmaintained for years.

When this happens inside the hook, React renders into an error state (the comparison runs during render, so the effect never re-runs and the component breaks).

Fix

Inline the fast-deep-equal/react comparator into src/misc/isDeepEqual.ts and guard the valueOf/toString shortcuts behind a real-method check (typeof … === "function"), so null-prototype objects fall through to the regular own-keys comparison. The React-specific _owner handling is preserved.

Behaviour is otherwise identical — verified by a 200,000-case fuzz comparing the inlined implementation against fast-deep-equal/react (numbers, strings, booleans, null/undefined/NaN, Date, RegExp, nested arrays/objects, Map, functions): 0 divergences on every input that does not use a null prototype.

The now-unused direct fast-deep-equal dependency is dropped from package.json (it remains in the tree transitively, so the lockfile is unchanged).

Tests

Added a regression test to tests/useDeepCompareEffect.test.ts that renders the hook with a Object.create(null) dependency and asserts the comparison never errors and the effect re-runs only on real change.

  • Fails on master: Received: [TypeError: a.valueOf is not a function]
  • Passes with this change.
  • Full suite green: 76 suites / 493 tests; tsc --noEmit and eslint clean.

`isDeepEqual` delegated to `fast-deep-equal/react`, which assumes every
object inherits `valueOf`/`toString` from `Object.prototype` and calls them
unconditionally. Objects created with `Object.create(null)` have neither, so
the comparison threw `TypeError: a.valueOf is not a function` (closes streamich#2700).

Inline the comparator and guard the `valueOf`/`toString` shortcuts behind a
real-method check so null-prototype objects fall through to the own-keys
comparison. Behaviour is otherwise identical (verified by a 200k-case fuzz
against the upstream module). The now-unused `fast-deep-equal` direct
dependency is dropped.
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.

useDeepCompareEffect throws error when comparing objects created with Object.create(null)

1 participant