Skip to content

Commit 94767fa

Browse files
committed
Memoize shallowEqual verdicts by object identity in useOnyx
1 parent e6b1c1c commit 94767fa

1 file changed

Lines changed: 38 additions & 2 deletions

File tree

lib/useOnyx.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,40 @@ type UseOnyxOptions<TKey extends OnyxKey, TReturnValue> = {
2828
selector?: UseOnyxSelector<TKey, TReturnValue>;
2929
};
3030

31+
// Memoizes shallowEqual verdicts by the identity of the compared objects. Onyx values are
32+
// treated as immutable (merge/set replace objects, never mutate), so a (prev, next) reference
33+
// pair always yields the same verdict. In the hot case — N no-selector hooks on the same big
34+
// key — every hook compares the exact same two cache-owned objects, so the first hook pays for
35+
// the O(keys) walk and the rest resolve in O(1). WeakMap keys make stale entries impossible to
36+
// read (lookup requires holding both exact objects) and let GC reclaim them.
37+
const shallowEqualVerdicts = new WeakMap<object, WeakMap<object, boolean>>();
38+
39+
// Identity-pair-memoized shallowEqual: same (prev, next) references → cached verdict, no walk.
40+
function memoizedShallowEqual(a: unknown, b: unknown): boolean {
41+
// Only object pairs are memoizable (WeakMap keys) — anything else is O(1) to compare anyway.
42+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
43+
return shallowEqual(a, b);
44+
}
45+
46+
let verdictsForA = shallowEqualVerdicts.get(a);
47+
48+
if (!verdictsForA) {
49+
verdictsForA = new WeakMap();
50+
shallowEqualVerdicts.set(a, verdictsForA);
51+
}
52+
53+
const cachedVerdict = verdictsForA.get(b);
54+
55+
if (cachedVerdict !== undefined) {
56+
return cachedVerdict;
57+
}
58+
59+
const verdict = shallowEqual(a, b);
60+
verdictsForA.set(b, verdict);
61+
62+
return verdict;
63+
}
64+
3165
type FetchStatus = 'loading' | 'loaded';
3266

3367
type ResultMetadata = {
@@ -201,9 +235,11 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
201235

202236
// shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
203237
// then falls back to comparing top-level properties for individual keys that may have
204-
// new references with equivalent content.
238+
// new references with equivalent content. The comparison is memoized by object identity
239+
// (see `memoizedShallowEqual`) so N hooks comparing the same two cache objects pay for
240+
// one walk in total instead of one walk each.
205241
// Normalize null to undefined to ensure consistent comparison (both represent "no value").
206-
const areValuesEqual = shallowEqual(previousValueRef.current ?? undefined, newValueRef.current ?? undefined);
242+
const areValuesEqual = memoizedShallowEqual(previousValueRef.current ?? undefined, newValueRef.current ?? undefined);
207243

208244
// We update the cached value and the result in the following conditions:
209245
// We will update the cached value and the result in any of the following situations:

0 commit comments

Comments
 (0)