Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"@types/js-cookie": "^3.0.0",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^3.0.0",
"nano-css": "^5.6.2",
Expand Down
81 changes: 79 additions & 2 deletions src/misc/isDeepEqual.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,80 @@
import isDeepEqualReact from 'fast-deep-equal/react';
// Based on `fast-deep-equal/react` (MIT, Evgeny Poberezkin), inlined so that
// objects with a `null` prototype (e.g. created via `Object.create(null)`) can be
// compared without throwing. The upstream module assumes every object inherits
// `valueOf`/`toString` from `Object.prototype` and calls them unconditionally,
// which throws `TypeError: a.valueOf is not a function` for null-prototype objects.
// See https://github.com/streamich/react-use/issues/2700 and
// https://github.com/epoberezkin/fast-deep-equal/issues/111.
const isDeepEqual = (a: any, b: any): boolean => {
if (a === b) {
return true;
}

export default isDeepEqualReact;
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (a.constructor !== b.constructor) {
return false;
}

let length: number;
let i: number;

if (Array.isArray(a)) {
length = a.length;
if (length !== b.length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (!isDeepEqual(a[i], b[i])) {
return false;
}
}
return true;
}

if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// Guard the `valueOf`/`toString` shortcuts behind a real-method check so
// null-prototype objects fall through to the own-keys comparison below.
if (a.valueOf !== Object.prototype.valueOf && typeof a.valueOf === 'function') {
return a.valueOf() === b.valueOf();
}
if (a.toString !== Object.prototype.toString && typeof a.toString === 'function') {
return a.toString() === b.toString();
}

const keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) {
return false;
}

for (i = length; i-- !== 0; ) {
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
return false;
}
}

for (i = length; i-- !== 0; ) {
const key = keys[i];

if (key === '_owner' && a.$$typeof) {
// React-specific: avoid traversing React elements' _owner, which contains
// circular references and is not needed when comparing the elements themselves.
continue;
}

if (!isDeepEqual(a[key], b[key])) {
return false;
}
}

return true;
}

// true if both NaN, false otherwise (`x !== x` is the canonical NaN check)
// eslint-disable-next-line no-self-compare
return a !== a && b !== b;
};

export default isDeepEqual;
26 changes: 26 additions & 0 deletions tests/useDeepCompareEffect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,29 @@ it('should run clean-up provided on unmount', () => {
unmount();
expect(mockEffectCleanup).toHaveBeenCalledTimes(1);
});

it('should not throw when comparing null-prototype objects', () => {
const mockEffect = jest.fn();
const createNullProtoDep = (value: number) => {
const dep = Object.create(null);
dep.value = value;
return dep;
};

let dep = createNullProtoDep(1);
const { rerender, result } = renderHook(() => useDeepCompareEffect(mockEffect, [dep]));
expect(result.error).toBeUndefined();
expect(mockEffect).toHaveBeenCalledTimes(1);

// Same shape -> comparison must not throw and the effect must not re-run.
dep = createNullProtoDep(1);
rerender();
expect(result.error).toBeUndefined();
expect(mockEffect).toHaveBeenCalledTimes(1);

// Different value -> comparison must not throw and the effect should re-run.
dep = createNullProtoDep(2);
rerender();
expect(result.error).toBeUndefined();
expect(mockEffect).toHaveBeenCalledTimes(2);
});