react-native-onyx 3.0.68 → 3.0.70

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/OnyxUtils.js CHANGED
@@ -231,6 +231,11 @@ function get(key) {
231
231
  // The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
232
232
  }
233
233
  }
234
+ // Prefer cache over stale storage if a concurrent write populated it during the read.
235
+ const cachedValue = OnyxCache_1.default.get(key);
236
+ if (cachedValue !== undefined) {
237
+ return cachedValue;
238
+ }
234
239
  if (val === undefined) {
235
240
  OnyxCache_1.default.addNullishStorageKey(key);
236
241
  return undefined;
@@ -420,8 +425,8 @@ function getCachedCollection(collectionKey, collectionMemberKeys) {
420
425
  }
421
426
  return filteredCollection;
422
427
  }
423
- // Return a copy to avoid mutations affecting the cache
424
- return Object.assign({}, collectionData);
428
+ // Snapshot is frozen safe to return by reference
429
+ return collectionData;
425
430
  }
426
431
  // Fallback to original implementation if collection data not available
427
432
  const collection = {};
@@ -445,70 +450,63 @@ function getCachedCollection(collectionKey, collectionMemberKeys) {
445
450
  * When a collection of keys change, search for any callbacks matching the collection key and trigger those callbacks
446
451
  */
447
452
  function keysChanged(collectionKey, partialCollection, partialPreviousCollection) {
448
- // We prepare the "cached collection" which is the entire collection + the new partial data that
449
- // was merged in via mergeCollection().
453
+ var _a;
450
454
  const cachedCollection = getCachedCollection(collectionKey);
451
455
  const previousCollection = partialPreviousCollection !== null && partialPreviousCollection !== void 0 ? partialPreviousCollection : {};
452
- // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
453
- // individual collection key member for the collection that is being updated. It is important to note that the collection parameter cane be a PARTIAL collection
454
- // and does not represent all of the combined keys and values for a collection key. It is just the "new" data that was merged in via mergeCollection().
455
- const stateMappingKeys = Object.keys(callbackToStateMapping);
456
- for (const stateMappingKey of stateMappingKeys) {
457
- const subscriber = callbackToStateMapping[stateMappingKey];
458
- if (!subscriber) {
459
- continue;
456
+ const changedMemberKeys = Object.keys(partialCollection !== null && partialCollection !== void 0 ? partialCollection : {});
457
+ // Use indexed lookup instead of scanning all subscribers.
458
+ // We need subscribers for: (1) the collection key itself, and (2) individual changed member keys.
459
+ const collectionSubscriberIDs = (_a = onyxKeyToSubscriptionIDs.get(collectionKey)) !== null && _a !== void 0 ? _a : [];
460
+ const memberSubscriberIDs = [];
461
+ for (const memberKey of changedMemberKeys) {
462
+ const ids = onyxKeyToSubscriptionIDs.get(memberKey);
463
+ if (ids) {
464
+ for (const id of ids) {
465
+ memberSubscriberIDs.push(id);
466
+ }
460
467
  }
461
- // Skip iteration if we do not have a collection key or a collection member key on this subscriber
462
- if (!Str.startsWith(subscriber.key, collectionKey)) {
468
+ }
469
+ // Notify collection-level subscribers
470
+ for (const subID of collectionSubscriberIDs) {
471
+ const subscriber = callbackToStateMapping[subID];
472
+ if (!subscriber || typeof subscriber.callback !== 'function') {
463
473
  continue;
464
474
  }
465
- /**
466
- * e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
467
- */
468
- const isSubscribedToCollectionKey = subscriber.key === collectionKey;
469
- /**
470
- * e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
471
- */
472
- const isSubscribedToCollectionMemberKey = OnyxKeys_1.default.isCollectionMemberKey(collectionKey, subscriber.key);
473
- // Regular Onyx.connect() subscriber found.
474
- if (typeof subscriber.callback === 'function') {
475
- try {
476
- // If they are subscribed to the collection key and using waitForCollectionCallback then we'll
477
- // send the whole cached collection.
478
- if (isSubscribedToCollectionKey) {
479
- lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
480
- if (subscriber.waitForCollectionCallback) {
481
- subscriber.callback(cachedCollection, subscriber.key, partialCollection);
482
- continue;
483
- }
484
- // If they are not using waitForCollectionCallback then we notify the subscriber with
485
- // the new merged data but only for any keys in the partial collection.
486
- const dataKeys = Object.keys(partialCollection !== null && partialCollection !== void 0 ? partialCollection : {});
487
- for (const dataKey of dataKeys) {
488
- if ((0, fast_equals_1.deepEqual)(cachedCollection[dataKey], previousCollection[dataKey])) {
489
- continue;
490
- }
491
- subscriber.callback(cachedCollection[dataKey], dataKey);
492
- }
493
- continue;
494
- }
495
- // And if the subscriber is specifically only tracking a particular collection member key then we will
496
- // notify them with the cached data for that key only.
497
- if (isSubscribedToCollectionMemberKey) {
498
- if ((0, fast_equals_1.deepEqual)(cachedCollection[subscriber.key], previousCollection[subscriber.key])) {
499
- continue;
500
- }
501
- const subscriberCallback = subscriber.callback;
502
- subscriberCallback(cachedCollection[subscriber.key], subscriber.key);
503
- lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection[subscriber.key], matchedKey: subscriber.key });
504
- continue;
505
- }
475
+ try {
476
+ lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
477
+ if (subscriber.waitForCollectionCallback) {
478
+ subscriber.callback(cachedCollection, subscriber.key, partialCollection);
506
479
  continue;
507
480
  }
508
- catch (error) {
509
- Logger.logAlert(`[OnyxUtils.keysChanged] Subscriber callback threw an error for key '${collectionKey}': ${error}`);
481
+ // Not using waitForCollectionCallback — notify per changed key
482
+ for (const dataKey of changedMemberKeys) {
483
+ if (cachedCollection[dataKey] === previousCollection[dataKey]) {
484
+ continue;
485
+ }
486
+ subscriber.callback(cachedCollection[dataKey], dataKey);
510
487
  }
511
488
  }
489
+ catch (error) {
490
+ Logger.logAlert(`[OnyxUtils.keysChanged] Subscriber callback threw an error for key '${collectionKey}': ${error}`);
491
+ }
492
+ }
493
+ // Notify member-level subscribers (e.g. subscribed to `report_123`)
494
+ for (const subID of memberSubscriberIDs) {
495
+ const subscriber = callbackToStateMapping[subID];
496
+ if (!subscriber || typeof subscriber.callback !== 'function') {
497
+ continue;
498
+ }
499
+ if (cachedCollection[subscriber.key] === previousCollection[subscriber.key]) {
500
+ continue;
501
+ }
502
+ try {
503
+ const subscriberCallback = subscriber.callback;
504
+ subscriberCallback(cachedCollection[subscriber.key], subscriber.key);
505
+ lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection[subscriber.key], matchedKey: subscriber.key });
506
+ }
507
+ catch (error) {
508
+ Logger.logAlert(`[OnyxUtils.keysChanged] Subscriber callback threw an error for key '${collectionKey}': ${error}`);
509
+ }
512
510
  }
513
511
  }
514
512
  /**
@@ -540,6 +538,9 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
540
538
  return;
541
539
  }
542
540
  }
541
+ // Cache the collection snapshot per dispatch so all subscribers to the same collection
542
+ // see a consistent view, even if an earlier subscriber's callback synchronously writes
543
+ // to the same collection.
543
544
  const cachedCollections = {};
544
545
  for (const stateMappingKey of stateMappingKeys) {
545
546
  const subscriber = callbackToStateMapping[stateMappingKey];
@@ -559,12 +560,13 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
559
560
  if (isProcessingCollectionUpdate) {
560
561
  continue;
561
562
  }
563
+ // Cache once per dispatch to ensure all subscribers see a consistent snapshot
564
+ // even if a previous callback synchronously wrote to the same collection.
562
565
  let cachedCollection = cachedCollections[subscriber.key];
563
566
  if (!cachedCollection) {
564
567
  cachedCollection = getCachedCollection(subscriber.key);
565
568
  cachedCollections[subscriber.key] = cachedCollection;
566
569
  }
567
- cachedCollection[key] = value;
568
570
  lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
569
571
  subscriber.callback(cachedCollection, subscriber.key, { [key]: value });
570
572
  continue;
package/dist/useOnyx.js CHANGED
@@ -62,16 +62,15 @@ function useOnyx(key, options, dependencies = []) {
62
62
  // Recompute if input changed, dependencies changed, or first time
63
63
  const dependenciesChanged = !(0, fast_equals_1.shallowEqual)(lastDependencies, currentDependencies);
64
64
  if (!hasComputed || lastInput !== input || dependenciesChanged) {
65
- // Only proceed if we have a valid selector
66
- if (selector) {
67
- const newOutput = selector(input);
68
- // Deep equality mode: only update if output actually changed
69
- if (!hasComputed || !(0, fast_equals_1.deepEqual)(lastOutput, newOutput) || dependenciesChanged) {
70
- lastInput = input;
71
- lastOutput = newOutput;
72
- lastDependencies = [...currentDependencies];
73
- hasComputed = true;
74
- }
65
+ const newOutput = selector(input);
66
+ // Always track the current input to avoid re-running the selector
67
+ // when the same input is seen again (even if the output didn't change).
68
+ lastInput = input;
69
+ // Only update the output reference if it actually changed
70
+ if (!hasComputed || !(0, fast_equals_1.deepEqual)(lastOutput, newOutput) || dependenciesChanged) {
71
+ lastOutput = newOutput;
72
+ lastDependencies = [...currentDependencies];
73
+ hasComputed = true;
75
74
  }
76
75
  }
77
76
  return lastOutput;
@@ -137,7 +136,7 @@ function useOnyx(key, options, dependencies = []) {
137
136
  // even if all other conditions (isFirstConnection, shouldGetCachedValue, key) are false.
138
137
  const lastComputedSelectorRef = (0, react_1.useRef)(memoizedSelector);
139
138
  const getSnapshot = (0, react_1.useCallback)(() => {
140
- var _a, _b, _c, _d;
139
+ var _a, _b, _c;
141
140
  // Check if we have any cache for this Onyx key
142
141
  // Don't use cache for first connection with initWithStoredValues: false
143
142
  // Also don't use cache during active data updates (when shouldGetCachedValueRef is true)
@@ -179,19 +178,11 @@ function useOnyx(key, options, dependencies = []) {
179
178
  newValueRef.current = undefined;
180
179
  newFetchStatus = 'loading';
181
180
  }
182
- // Optimized equality checking:
183
- // - Memoized selectors already handle deep equality internally, so we can use fast reference equality
184
- // - Non-selector cases use shallow equality for object reference checks
185
- // - Normalize null to undefined to ensure consistent comparison (both represent "no value")
186
- let areValuesEqual;
187
- if (memoizedSelector) {
188
- const normalizedPrevious = (_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined;
189
- const normalizedNew = (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined;
190
- areValuesEqual = normalizedPrevious === normalizedNew;
191
- }
192
- else {
193
- areValuesEqual = (0, fast_equals_1.shallowEqual)((_c = previousValueRef.current) !== null && _c !== void 0 ? _c : undefined, newValueRef.current);
194
- }
181
+ // shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
182
+ // then falls back to comparing top-level properties for individual keys that may have
183
+ // new references with equivalent content.
184
+ // Normalize null to undefined to ensure consistent comparison (both represent "no value").
185
+ 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);
195
186
  // We update the cached value and the result in the following conditions:
196
187
  // We will update the cached value and the result in any of the following situations:
197
188
  // - The previously cached value is different from the new value.
@@ -205,7 +196,7 @@ function useOnyx(key, options, dependencies = []) {
205
196
  // If the new value is `null` we default it to `undefined` to ensure the consumer gets a consistent result from the hook.
206
197
  newFetchStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded';
207
198
  resultRef.current = [
208
- (_d = previousValueRef.current) !== null && _d !== void 0 ? _d : undefined,
199
+ (_c = previousValueRef.current) !== null && _c !== void 0 ? _c : undefined,
209
200
  {
210
201
  status: newFetchStatus,
211
202
  sourceValue: sourceValueRef.current,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.68",
3
+ "version": "3.0.70",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",