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..ecf549c91325
--- /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('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)
+ 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 = {local: 2, shared: 1}
+ 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..d1d96f39ada5
--- /dev/null
+++ b/shared/util/shallow-equal.ts
@@ -0,0 +1,38 @@
+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 (!isObjectLike(objA) || !isObjectLike(objB)) {
+ return false
+ }
+
+ 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
+ }
+
+ for (const key of keysA) {
+
+ if (!Object.prototype.hasOwnProperty.call(recordB, key)) {
+ return false
+ }
+
+ const valueA = recordA[key]
+ const valueB = recordB[key]
+
+ if (valueA !== valueB) {
+ return false
+ }
+ }
+
+ return true
+}
+
+export default shallowEqual
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"