react-native-onyx 3.0.92 → 3.0.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/OnyxCache.js CHANGED
@@ -286,7 +286,7 @@ class OnyxCache {
286
286
  // Initialize frozen snapshots for collection keys
287
287
  for (const collectionKey of collectionKeys) {
288
288
  if (!this.collectionSnapshots.has(collectionKey)) {
289
- this.collectionSnapshots.set(collectionKey, Object.freeze({}));
289
+ this.collectionSnapshots.set(collectionKey, FROZEN_EMPTY_COLLECTION);
290
290
  }
291
291
  }
292
292
  }
@@ -302,6 +302,7 @@ class OnyxCache {
302
302
  const previousSnapshot = this.collectionSnapshots.get(collectionKey);
303
303
  const members = {};
304
304
  let hasMemberChanges = false;
305
+ let hasMembers = false;
305
306
  // Use the indexed forward lookup for O(collectionMembers) iteration.
306
307
  // Falls back to scanning all storageKeys if the index isn't populated yet.
307
308
  const memberKeys = OnyxKeys_1.default.getMembersOfCollection(collectionKey);
@@ -318,6 +319,7 @@ class OnyxCache {
318
319
  // and should not be included in the frozen collection snapshot.
319
320
  if (val !== undefined && val !== null) {
320
321
  members[key] = val;
322
+ hasMembers = true;
321
323
  // Check if this member's reference changed from the old snapshot
322
324
  if (!hasMemberChanges && (!previousSnapshot || previousSnapshot[key] !== val)) {
323
325
  hasMemberChanges = true;
@@ -342,6 +344,13 @@ class OnyxCache {
342
344
  if (!hasMemberChanges && previousSnapshot) {
343
345
  return;
344
346
  }
347
+ // When the collection has no members, reuse one shared empty object for every empty
348
+ // collection. That way reads can tell a collection is empty with a quick `===` check
349
+ // instead of looping over its keys every time.
350
+ if (!hasMembers) {
351
+ this.collectionSnapshots.set(collectionKey, FROZEN_EMPTY_COLLECTION);
352
+ return;
353
+ }
345
354
  Object.freeze(members);
346
355
  this.collectionSnapshots.set(collectionKey, members);
347
356
  }
@@ -356,17 +365,18 @@ class OnyxCache {
356
365
  this.dirtyCollections.delete(collectionKey);
357
366
  }
358
367
  const snapshot = this.collectionSnapshots.get(collectionKey);
359
- if (utils_1.default.isEmptyObject(snapshot)) {
360
- // We check storageKeys.size (not collection-specific keys) to distinguish
361
- // "init complete, this collection is genuinely empty" from "init not done yet."
362
- // During init, setAllKeys loads ALL keys at once — so if any key exists,
363
- // the full storage picture is loaded and an empty collection is truly empty.
364
- // Returning undefined before init prevents subscribers from seeing a false empty state.
365
- if (this.storageKeys.size > 0) {
366
- return FROZEN_EMPTY_COLLECTION;
367
- }
368
+ // We never stored anything for this collection key.
369
+ if (snapshot === undefined) {
368
370
  return undefined;
369
371
  }
372
+ // The collection is empty (it holds our shared empty object). But "empty" is ambiguous
373
+ // during startup: we can't tell an actually-empty collection apart from one whose data
374
+ // hasn't loaded yet. Once any key exists, we know setAllKeys has run and loaded everything,
375
+ // so an empty collection really is empty. Before that, return undefined so subscribers
376
+ // don't briefly see a collection as empty when it just hasn't loaded.
377
+ if (snapshot === FROZEN_EMPTY_COLLECTION) {
378
+ return this.storageKeys.size > 0 ? FROZEN_EMPTY_COLLECTION : undefined;
379
+ }
370
380
  return snapshot;
371
381
  }
372
382
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Identity-pair-memoized shallowEqual: same (a, b) references → cached verdict, no walk.
3
+ */
4
+ declare function memoizedShallowEqual(a: unknown, b: unknown): boolean;
5
+ export default memoizedShallowEqual;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fast_equals_1 = require("fast-equals");
4
+ /**
5
+ * Memoizes shallowEqual verdicts by the identity of the compared objects. Onyx values are
6
+ * treated as immutable (merge/set replace objects, never mutate), so a (prev, next) reference
7
+ * pair always yields the same verdict. In the hot case — N no-selector hooks on the same big
8
+ * key — every hook compares the exact same two cache-owned objects, so the first hook pays for
9
+ * the O(keys) walk and the rest resolve in O(1). WeakMap keys make stale entries impossible to
10
+ * read (lookup requires holding both exact objects) and let GC reclaim them.
11
+ */
12
+ const shallowEqualVerdicts = new WeakMap();
13
+ /**
14
+ * Identity-pair-memoized shallowEqual: same (a, b) references → cached verdict, no walk.
15
+ */
16
+ function memoizedShallowEqual(a, b) {
17
+ // Only object pairs are memoizable (WeakMap keys) — anything else is O(1) to compare anyway.
18
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
19
+ return (0, fast_equals_1.shallowEqual)(a, b);
20
+ }
21
+ let verdictsForA = shallowEqualVerdicts.get(a);
22
+ if (!verdictsForA) {
23
+ verdictsForA = new WeakMap();
24
+ shallowEqualVerdicts.set(a, verdictsForA);
25
+ }
26
+ const cachedVerdict = verdictsForA.get(b);
27
+ if (cachedVerdict !== undefined) {
28
+ return cachedVerdict;
29
+ }
30
+ const verdict = (0, fast_equals_1.shallowEqual)(a, b);
31
+ verdictsForA.set(b, verdict);
32
+ return verdict;
33
+ }
34
+ exports.default = memoizedShallowEqual;
package/dist/useOnyx.js CHANGED
@@ -42,6 +42,7 @@ const OnyxCache_1 = __importStar(require("./OnyxCache"));
42
42
  const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager"));
43
43
  const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
44
44
  const OnyxSnapshotCache_1 = __importDefault(require("./OnyxSnapshotCache"));
45
+ const memoizedShallowEqual_1 = __importDefault(require("./memoizedShallowEqual"));
45
46
  const useLiveRef_1 = __importDefault(require("./useLiveRef"));
46
47
  function useOnyx(key, options, dependencies = []) {
47
48
  const connectionRef = (0, react_1.useRef)(null);
@@ -170,9 +171,11 @@ function useOnyx(key, options, dependencies = []) {
170
171
  }
171
172
  // shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
172
173
  // then falls back to comparing top-level properties for individual keys that may have
173
- // new references with equivalent content.
174
+ // new references with equivalent content. The comparison is memoized by object identity
175
+ // (see `memoizedShallowEqual`) so N hooks comparing the same two cache objects pay for
176
+ // one walk in total instead of one walk each.
174
177
  // Normalize null to undefined to ensure consistent comparison (both represent "no value").
175
- const areValuesEqual = (0, fast_equals_1.shallowEqual)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined);
178
+ const areValuesEqual = (0, memoizedShallowEqual_1.default)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined);
176
179
  // We update the cached value and the result in the following conditions:
177
180
  // We will update the cached value and the result in any of the following situations:
178
181
  // - The previously cached value is different from the new value.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.92",
3
+ "version": "3.0.94",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",