From 4c841dfa95a9f7b7c66f89fddf10edc32aa149b9 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Mon, 6 Apr 2026 15:37:28 -0400 Subject: [PATCH 1/4] remove shallowequal --- .../conversation/list-area/index.desktop.tsx | 2 +- .../popup/floating-box/index.desktop.tsx | 2 +- shared/constants/utils.tsx | 2 +- shared/package.json | 2 - shared/util/shallow-equal.test.ts | 50 +++++++++++++++++++ shared/util/shallow-equal.ts | 39 +++++++++++++++ 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 shared/util/shallow-equal.test.ts create mode 100644 shared/util/shallow-equal.ts diff --git a/shared/chat/conversation/list-area/index.desktop.tsx b/shared/chat/conversation/list-area/index.desktop.tsx index d2c44a0a614b..25692d01bc6c 100644 --- a/shared/chat/conversation/list-area/index.desktop.tsx +++ b/shared/chat/conversation/list-area/index.desktop.tsx @@ -15,7 +15,7 @@ import {globalMargins} from '@/styles/shared' import {FocusContext, ScrollContext} from '../normal/context' import {chatDebugEnabled} from '@/constants/chat/debug' import logger from '@/logger' -import shallowEqual from 'shallowequal' +import shallowEqual from '@/util/shallow-equal' import useResizeObserver from '@/util/use-resize-observer.desktop' import useIntersectionObserver from '@/util/use-intersection-observer' import {useConfigState} from '@/stores/config' diff --git a/shared/common-adapters/popup/floating-box/index.desktop.tsx b/shared/common-adapters/popup/floating-box/index.desktop.tsx index db49422d4672..a54ce2747524 100644 --- a/shared/common-adapters/popup/floating-box/index.desktop.tsx +++ b/shared/common-adapters/popup/floating-box/index.desktop.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import type {Props} from '.' +import shallowEqual from '@/util/shallow-equal' import {RelativeFloatingBox} from './relative-floating-box.desktop' import noop from 'lodash/noop' -import shallowEqual from 'shallowequal' const FloatingBox = (props: Props) => { const {attachTo, disableEscapeKey, position, positionFallbacks, children, offset} = props diff --git a/shared/constants/utils.tsx b/shared/constants/utils.tsx index 0434e357b603..c9bc306e0b04 100644 --- a/shared/constants/utils.tsx +++ b/shared/constants/utils.tsx @@ -40,7 +40,7 @@ export const useNav = () => { } export {wrapErrors} from '@/util/debug' -export {default as shallowEqual} from 'shallowequal' +export {default as shallowEqual} from '@/util/shallow-equal' export {useDebouncedCallback, useThrottledCallback, type DebouncedState} from 'use-debounce' export {useShallow, useDeep} from '@/util/zustand' export {default as useRPC} from '@/util/use-rpc' diff --git a/shared/package.json b/shared/package.json index 2e92db7f2e39..6608b6e5b454 100644 --- a/shared/package.json +++ b/shared/package.json @@ -135,7 +135,6 @@ "react-native-webview": "13.16.1", "react-native-worklets": "0.8.1", "react-native-zoom-toolkit": "5.0.1", - "shallowequal": "1.1.0", "use-debounce": "10.1.1", "util": "0.12.5", "zustand": "5.0.12" @@ -161,7 +160,6 @@ "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/react-measure": "2.0.12", - "@types/shallowequal": "1.1.5", "@types/webpack-env": "1.18.8", "@typescript/native-preview": "7.0.0-dev.20260331.1", "@testing-library/dom": "10.4.1", diff --git a/shared/util/shallow-equal.test.ts b/shared/util/shallow-equal.test.ts new file mode 100644 index 000000000000..2db2da7b7753 --- /dev/null +++ b/shared/util/shallow-equal.test.ts @@ -0,0 +1,50 @@ +/// +import shallowEqual from './shallow-equal' + +test('returns true for the same reference', () => { + const value = {a: 1} + + expect(shallowEqual(value, value)).toBe(true) +}) + +test('returns false when either side is null or a primitive', () => { + expect(shallowEqual(null, {})).toBe(false) + expect(shallowEqual({}, null)).toBe(false) + expect(shallowEqual(1, 1)).toBe(true) + expect(shallowEqual(1, 2)).toBe(false) + expect(shallowEqual(1, {})).toBe(false) +}) + +test('compares arrays shallowly', () => { + expect(shallowEqual([1, 2], [1, 2])).toBe(true) + expect(shallowEqual([1, {nested: true}], [1, {nested: true}])).toBe(false) +}) + +test('only compares own enumerable keys', () => { + const proto = {shared: 1} + const a = Object.create(proto) as {local?: number} + const b = Object.create(proto) as {local?: number} + + a.local = 2 + b.local = 2 + + expect(shallowEqual(a, b)).toBe(true) + + const withOwnShared = {shared: 1, local: 2} + expect(shallowEqual(a, withOwnShared)).toBe(false) +}) + +test('ignores non-enumerable properties', () => { + const a = {visible: 1} + const b = {visible: 1} + + Object.defineProperty(a, 'hidden', {enumerable: false, value: 1}) + Object.defineProperty(b, 'hidden', {enumerable: false, value: 2}) + + expect(shallowEqual(a, b)).toBe(true) +}) + +test('uses strict equality for leaf values', () => { + expect(shallowEqual({a: Number.NaN}, {a: Number.NaN})).toBe(false) + expect(shallowEqual({a: undefined}, {a: undefined})).toBe(true) +}) diff --git a/shared/util/shallow-equal.ts b/shared/util/shallow-equal.ts new file mode 100644 index 000000000000..5f508c57ede4 --- /dev/null +++ b/shared/util/shallow-equal.ts @@ -0,0 +1,39 @@ +const hasOwnProperty = Object.prototype.hasOwnProperty + +const shallowEqual = (objA: any, objB: any): boolean => { + if (objA === objB) { + return true + } + + if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) { + return false + } + + const keysA = Object.keys(objA) + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) { + return false + } + + const bHasOwnProperty = hasOwnProperty.bind(objB) as (key: string) => boolean + + for (let idx = 0; idx < keysA.length; idx++) { + const key = keysA[idx]! + + if (!bHasOwnProperty(key)) { + return false + } + + const valueA = objA[key] + const valueB = objB[key] + + if (valueA !== valueB) { + return false + } + } + + return true +} + +export default shallowEqual From 3a2da1a9c64d2705135e7cc459f6c5a8557fd26f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Mon, 6 Apr 2026 15:38:15 -0400 Subject: [PATCH 2/4] WIP --- shared/yarn.lock | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/shared/yarn.lock b/shared/yarn.lock index d5bcf221dbc5..822e6166eead 100644 --- a/shared/yarn.lock +++ b/shared/yarn.lock @@ -3625,11 +3625,6 @@ "@types/http-errors" "*" "@types/node" "*" -"@types/shallowequal@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@types/shallowequal/-/shallowequal-1.1.5.tgz#37e4871c464981b4abee74990c73c8f414cd13dd" - integrity sha512-8afr1hbNqvZ/FBMY2mcfkkbk7xhlTZN4lVCgQf55YdjUQpWLemmrcvcHg94vjw+ZVIfPa3UZz/sOE6CkaMlDnQ== - "@types/sockjs@^0.3.36": version "0.3.36" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" @@ -11351,11 +11346,6 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" -shallowequal@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -12164,11 +12154,6 @@ ua-parser-js@^1.0.35: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.41.tgz#bd04dc9ec830fcf9e4fad35cf22dcedd2e3b4e9c" integrity sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug== -uint8array-extras@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz#10d2a85213de3ada304fea1c454f635c73839e86" - integrity sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A== - unbox-primitive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" From 15758ca46830c7be79c6b9f8e255ec6947224741 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Mon, 6 Apr 2026 15:57:26 -0400 Subject: [PATCH 3/4] WIP --- shared/util/shallow-equal.test.ts | 2 +- shared/util/shallow-equal.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/shared/util/shallow-equal.test.ts b/shared/util/shallow-equal.test.ts index 2db2da7b7753..adefb9ef4dbf 100644 --- a/shared/util/shallow-equal.test.ts +++ b/shared/util/shallow-equal.test.ts @@ -7,7 +7,7 @@ test('returns true for the same reference', () => { expect(shallowEqual(value, value)).toBe(true) }) -test('returns false when either side is null or a primitive', () => { +test('uses strict equality for primitives and rejects null/object mismatches', () => { expect(shallowEqual(null, {})).toBe(false) expect(shallowEqual({}, null)).toBe(false) expect(shallowEqual(1, 1)).toBe(true) diff --git a/shared/util/shallow-equal.ts b/shared/util/shallow-equal.ts index 5f508c57ede4..a9aea0596740 100644 --- a/shared/util/shallow-equal.ts +++ b/shared/util/shallow-equal.ts @@ -1,22 +1,26 @@ const hasOwnProperty = Object.prototype.hasOwnProperty -const shallowEqual = (objA: any, objB: any): boolean => { +const isObjectLike = (value: unknown): value is object => typeof value === 'object' && value !== null + +const shallowEqual = (objA: unknown, objB: unknown): boolean => { if (objA === objB) { return true } - if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) { + if (!isObjectLike(objA) || !isObjectLike(objB)) { return false } - const keysA = Object.keys(objA) - const keysB = Object.keys(objB) + const recordA = objA as Record + const recordB = objB as Record + const keysA = Object.keys(recordA) + const keysB = Object.keys(recordB) if (keysA.length !== keysB.length) { return false } - const bHasOwnProperty = hasOwnProperty.bind(objB) as (key: string) => boolean + const bHasOwnProperty = hasOwnProperty.bind(recordB) as (key: string) => boolean for (let idx = 0; idx < keysA.length; idx++) { const key = keysA[idx]! @@ -25,8 +29,8 @@ const shallowEqual = (objA: any, objB: any): boolean => { return false } - const valueA = objA[key] - const valueB = objB[key] + const valueA = recordA[key] + const valueB = recordB[key] if (valueA !== valueB) { return false From fbf79a837addfd9e47de3bea395b9fe1735620aa Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Mon, 6 Apr 2026 16:02:18 -0400 Subject: [PATCH 4/4] WIP --- shared/util/shallow-equal.test.ts | 2 +- shared/util/shallow-equal.ts | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/shared/util/shallow-equal.test.ts b/shared/util/shallow-equal.test.ts index adefb9ef4dbf..ecf549c91325 100644 --- a/shared/util/shallow-equal.test.ts +++ b/shared/util/shallow-equal.test.ts @@ -30,7 +30,7 @@ test('only compares own enumerable keys', () => { expect(shallowEqual(a, b)).toBe(true) - const withOwnShared = {shared: 1, local: 2} + const withOwnShared = {local: 2, shared: 1} expect(shallowEqual(a, withOwnShared)).toBe(false) }) diff --git a/shared/util/shallow-equal.ts b/shared/util/shallow-equal.ts index a9aea0596740..d1d96f39ada5 100644 --- a/shared/util/shallow-equal.ts +++ b/shared/util/shallow-equal.ts @@ -1,5 +1,3 @@ -const hasOwnProperty = Object.prototype.hasOwnProperty - const isObjectLike = (value: unknown): value is object => typeof value === 'object' && value !== null const shallowEqual = (objA: unknown, objB: unknown): boolean => { @@ -20,12 +18,9 @@ const shallowEqual = (objA: unknown, objB: unknown): boolean => { return false } - const bHasOwnProperty = hasOwnProperty.bind(recordB) as (key: string) => boolean - - for (let idx = 0; idx < keysA.length; idx++) { - const key = keysA[idx]! + for (const key of keysA) { - if (!bHasOwnProperty(key)) { + if (!Object.prototype.hasOwnProperty.call(recordB, key)) { return false }