react-native-onyx 3.0.69 → 3.0.71

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
@@ -425,8 +425,8 @@ function getCachedCollection(collectionKey, collectionMemberKeys) {
425
425
  }
426
426
  return filteredCollection;
427
427
  }
428
- // Return a copy to avoid mutations affecting the cache
429
- return Object.assign({}, collectionData);
428
+ // Snapshot is frozen safe to return by reference
429
+ return collectionData;
430
430
  }
431
431
  // Fallback to original implementation if collection data not available
432
432
  const collection = {};
@@ -450,70 +450,63 @@ function getCachedCollection(collectionKey, collectionMemberKeys) {
450
450
  * When a collection of keys change, search for any callbacks matching the collection key and trigger those callbacks
451
451
  */
452
452
  function keysChanged(collectionKey, partialCollection, partialPreviousCollection) {
453
- // We prepare the "cached collection" which is the entire collection + the new partial data that
454
- // was merged in via mergeCollection().
453
+ var _a;
455
454
  const cachedCollection = getCachedCollection(collectionKey);
456
455
  const previousCollection = partialPreviousCollection !== null && partialPreviousCollection !== void 0 ? partialPreviousCollection : {};
457
- // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
458
- // 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
459
- // 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().
460
- const stateMappingKeys = Object.keys(callbackToStateMapping);
461
- for (const stateMappingKey of stateMappingKeys) {
462
- const subscriber = callbackToStateMapping[stateMappingKey];
463
- if (!subscriber) {
464
- 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
+ }
465
467
  }
466
- // Skip iteration if we do not have a collection key or a collection member key on this subscriber
467
- 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') {
468
473
  continue;
469
474
  }
470
- /**
471
- * e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
472
- */
473
- const isSubscribedToCollectionKey = subscriber.key === collectionKey;
474
- /**
475
- * e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
476
- */
477
- const isSubscribedToCollectionMemberKey = OnyxKeys_1.default.isCollectionMemberKey(collectionKey, subscriber.key);
478
- // Regular Onyx.connect() subscriber found.
479
- if (typeof subscriber.callback === 'function') {
480
- try {
481
- // If they are subscribed to the collection key and using waitForCollectionCallback then we'll
482
- // send the whole cached collection.
483
- if (isSubscribedToCollectionKey) {
484
- lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
485
- if (subscriber.waitForCollectionCallback) {
486
- subscriber.callback(cachedCollection, subscriber.key, partialCollection);
487
- continue;
488
- }
489
- // If they are not using waitForCollectionCallback then we notify the subscriber with
490
- // the new merged data but only for any keys in the partial collection.
491
- const dataKeys = Object.keys(partialCollection !== null && partialCollection !== void 0 ? partialCollection : {});
492
- for (const dataKey of dataKeys) {
493
- if ((0, fast_equals_1.deepEqual)(cachedCollection[dataKey], previousCollection[dataKey])) {
494
- continue;
495
- }
496
- subscriber.callback(cachedCollection[dataKey], dataKey);
497
- }
498
- continue;
499
- }
500
- // And if the subscriber is specifically only tracking a particular collection member key then we will
501
- // notify them with the cached data for that key only.
502
- if (isSubscribedToCollectionMemberKey) {
503
- if ((0, fast_equals_1.deepEqual)(cachedCollection[subscriber.key], previousCollection[subscriber.key])) {
504
- continue;
505
- }
506
- const subscriberCallback = subscriber.callback;
507
- subscriberCallback(cachedCollection[subscriber.key], subscriber.key);
508
- lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection[subscriber.key], matchedKey: subscriber.key });
509
- continue;
510
- }
475
+ try {
476
+ lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
477
+ if (subscriber.waitForCollectionCallback) {
478
+ subscriber.callback(cachedCollection, subscriber.key, partialCollection);
511
479
  continue;
512
480
  }
513
- catch (error) {
514
- 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);
515
487
  }
516
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
+ }
517
510
  }
518
511
  }
519
512
  /**
@@ -545,6 +538,9 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
545
538
  return;
546
539
  }
547
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.
548
544
  const cachedCollections = {};
549
545
  for (const stateMappingKey of stateMappingKeys) {
550
546
  const subscriber = callbackToStateMapping[stateMappingKey];
@@ -564,12 +560,13 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
564
560
  if (isProcessingCollectionUpdate) {
565
561
  continue;
566
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.
567
565
  let cachedCollection = cachedCollections[subscriber.key];
568
566
  if (!cachedCollection) {
569
567
  cachedCollection = getCachedCollection(subscriber.key);
570
568
  cachedCollections[subscriber.key] = cachedCollection;
571
569
  }
572
- cachedCollection[key] = value;
573
570
  lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
574
571
  subscriber.callback(cachedCollection, subscriber.key, { [key]: value });
575
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;
@@ -97,6 +96,9 @@ function useOnyx(key, options, dependencies = []) {
97
96
  // after cleanup), so the hook automatically enters first-connection mode for the new key without any
98
97
  // explicit reset logic — eliminating the race condition where cleanup could clobber a boolean flag.
99
98
  const connectedKeyRef = (0, react_1.useRef)(null);
99
+ // Tracks whether the hook has completed its initial mount subscription.
100
+ // Unlike connectedKeyRef (which gets nulled by cleanup), this persists across re-subscriptions.
101
+ const hasMountedRef = (0, react_1.useRef)(false);
100
102
  // Indicates if the hook is connecting to an Onyx key.
101
103
  const isConnectingRef = (0, react_1.useRef)(false);
102
104
  // Stores the `onStoreChange()` function, which can be used to trigger a `getSnapshot()` update when desired.
@@ -137,7 +139,7 @@ function useOnyx(key, options, dependencies = []) {
137
139
  // even if all other conditions (isFirstConnection, shouldGetCachedValue, key) are false.
138
140
  const lastComputedSelectorRef = (0, react_1.useRef)(memoizedSelector);
139
141
  const getSnapshot = (0, react_1.useCallback)(() => {
140
- var _a, _b, _c, _d;
142
+ var _a, _b, _c;
141
143
  // Check if we have any cache for this Onyx key
142
144
  // Don't use cache for first connection with initWithStoredValues: false
143
145
  // Also don't use cache during active data updates (when shouldGetCachedValueRef is true)
@@ -179,19 +181,11 @@ function useOnyx(key, options, dependencies = []) {
179
181
  newValueRef.current = undefined;
180
182
  newFetchStatus = 'loading';
181
183
  }
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
- }
184
+ // shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
185
+ // then falls back to comparing top-level properties for individual keys that may have
186
+ // new references with equivalent content.
187
+ // Normalize null to undefined to ensure consistent comparison (both represent "no value").
188
+ 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
189
  // We update the cached value and the result in the following conditions:
196
190
  // We will update the cached value and the result in any of the following situations:
197
191
  // - The previously cached value is different from the new value.
@@ -205,7 +199,7 @@ function useOnyx(key, options, dependencies = []) {
205
199
  // If the new value is `null` we default it to `undefined` to ensure the consumer gets a consistent result from the hook.
206
200
  newFetchStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded';
207
201
  resultRef.current = [
208
- (_d = previousValueRef.current) !== null && _d !== void 0 ? _d : undefined,
202
+ (_c = previousValueRef.current) !== null && _c !== void 0 ? _c : undefined,
209
203
  {
210
204
  status: newFetchStatus,
211
205
  sourceValue: sourceValueRef.current,
@@ -220,11 +214,19 @@ function useOnyx(key, options, dependencies = []) {
220
214
  const subscribe = (0, react_1.useCallback)((onStoreChange) => {
221
215
  // Reset internal state so the hook properly transitions through loading
222
216
  // for the new key instead of preserving stale state from the previous one.
223
- previousValueRef.current = null;
224
- newValueRef.current = null;
217
+ // Only reset when the key has actually changed (not on initial mount).
218
+ if (hasMountedRef.current) {
219
+ previousValueRef.current = null;
220
+ newValueRef.current = null;
221
+ sourceValueRef.current = undefined;
222
+ resultRef.current = [undefined, { status: (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false ? 'loaded' : 'loading' }];
223
+ }
224
+ // Force a cache re-read on every (re)subscription so any side effects from
225
+ // subscribeToKey (e.g. addNullishStorageKey for skippable collection member ids)
226
+ // are reflected in the next getSnapshot. Resetting this flag does not change
227
+ // resultRef by itself, so it doesn't cause an extra mount render.
225
228
  shouldGetCachedValueRef.current = true;
226
- sourceValueRef.current = undefined;
227
- resultRef.current = [undefined, { status: (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false ? 'loaded' : 'loading' }];
229
+ hasMountedRef.current = true;
228
230
  isConnectingRef.current = true;
229
231
  onStoreChangeFnRef.current = onStoreChange;
230
232
  connectionRef.current = OnyxConnectionManager_1.default.connect({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.69",
3
+ "version": "3.0.71",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",