react-native-onyx 3.0.18 → 3.0.20

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
@@ -465,6 +465,81 @@ function tryGetCachedValue(key) {
465
465
  }
466
466
  return val;
467
467
  }
468
+ /**
469
+ * Marks items that existed in previousCollection but not in preservedCollection as null.
470
+ * This ensures subscribers are properly notified about item removals.
471
+ * @param preservedCollection - The collection to mark removed items in (mutated in place)
472
+ * @param previousCollection - The previous collection state to compare against
473
+ */
474
+ function markRemovedItemsAsNull(preservedCollection, previousCollection) {
475
+ if (!previousCollection) {
476
+ return preservedCollection;
477
+ }
478
+ const mutablePreservedCollection = Object.assign({}, preservedCollection);
479
+ Object.keys(previousCollection).forEach((key) => {
480
+ if (key in preservedCollection) {
481
+ return;
482
+ }
483
+ mutablePreservedCollection[key] = null;
484
+ });
485
+ return mutablePreservedCollection;
486
+ }
487
+ /**
488
+ * Utility function to preserve object references for unchanged items in collection operations.
489
+ * Compares new values with cached values using deep equality and preserves references when data is identical.
490
+ * @param keyValuePairs - Array of key-value pairs to process
491
+ * @param previousCollection - Optional previous collection state. If provided, removed items will be included as null
492
+ * @returns The preserved collection with unchanged references maintained and removed items marked as null
493
+ */
494
+ function preserveCollectionReferences(keyValuePairs, previousCollection) {
495
+ const preservedCollection = {};
496
+ keyValuePairs.forEach(([key, value]) => {
497
+ const cachedValue = OnyxCache_1.default.get(key, false);
498
+ // If no cached value exists, we need to add the new value (skip expensive deep equality check)
499
+ // Use deep equality check to preserve references for unchanged items
500
+ if (cachedValue !== undefined && (0, fast_equals_1.deepEqual)(value, cachedValue)) {
501
+ // Keep the existing reference
502
+ preservedCollection[key] = cachedValue;
503
+ }
504
+ else {
505
+ // Update cache only for changed items
506
+ OnyxCache_1.default.set(key, value);
507
+ preservedCollection[key] = value;
508
+ }
509
+ });
510
+ if (previousCollection) {
511
+ return markRemovedItemsAsNull(preservedCollection, previousCollection);
512
+ }
513
+ return preservedCollection;
514
+ }
515
+ /**
516
+ * Utility function for merge operations that preserves references after cache merge has been performed.
517
+ * Compares merged values with original cached values and preserves references when data is unchanged.
518
+ * @param collection - Collection of merged data
519
+ * @param originalCachedValues - Original cached values before merge
520
+ * @param previousCollection - Optional previous collection state. If provided, removed items will be included as null
521
+ * @returns The preserved collection with unchanged references maintained and removed items marked as null
522
+ */
523
+ function preserveCollectionReferencesAfterMerge(collection, originalCachedValues, previousCollection) {
524
+ const preservedCollection = {};
525
+ Object.keys(collection).forEach((key) => {
526
+ const newMergedValue = OnyxCache_1.default.get(key, false);
527
+ const originalValue = originalCachedValues[key];
528
+ // Use deep equality check to preserve references for unchanged items
529
+ if (originalValue !== undefined && (0, fast_equals_1.deepEqual)(newMergedValue, originalValue)) {
530
+ // Keep the existing reference and update cache
531
+ preservedCollection[key] = originalValue;
532
+ OnyxCache_1.default.set(key, originalValue);
533
+ }
534
+ else {
535
+ preservedCollection[key] = newMergedValue;
536
+ }
537
+ });
538
+ if (previousCollection) {
539
+ return markRemovedItemsAsNull(preservedCollection, previousCollection);
540
+ }
541
+ return preservedCollection;
542
+ }
468
543
  function getCachedCollection(collectionKey, collectionMemberKeys) {
469
544
  // Use optimized collection data retrieval when cache is populated
470
545
  const collectionData = OnyxCache_1.default.getCollectionData(collectionKey);
@@ -1195,8 +1270,9 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1195
1270
  });
1196
1271
  const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
1197
1272
  const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
1198
- keyValuePairs.forEach(([key, value]) => OnyxCache_1.default.set(key, value));
1199
- const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
1273
+ // Preserve references for unchanged items and include removed items as null in setCollection
1274
+ const preservedCollection = preserveCollectionReferences(keyValuePairs, previousCollection);
1275
+ const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
1200
1276
  return storage_1.default.multiSet(keyValuePairs)
1201
1277
  .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt))
1202
1278
  .then(() => {
@@ -1248,6 +1324,8 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1248
1324
  resultCollectionKeys = Object.keys(resultCollection);
1249
1325
  return getAllKeys()
1250
1326
  .then((persistedKeys) => {
1327
+ // Capture keys that will be removed (before calling remove())
1328
+ const keysToRemove = resultCollectionKeys.filter((key) => resultCollection[key] === null && persistedKeys.has(key));
1251
1329
  // Split to keys that exist in storage and keys that don't
1252
1330
  const keys = resultCollectionKeys.filter((key) => {
1253
1331
  if (resultCollection[key] === null) {
@@ -1257,6 +1335,8 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1257
1335
  return true;
1258
1336
  });
1259
1337
  const existingKeys = keys.filter((key) => persistedKeys.has(key));
1338
+ // Get previous values for both existing keys and keys that will be removed
1339
+ const allAffectedKeys = [...existingKeys, ...keysToRemove];
1260
1340
  const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys);
1261
1341
  const existingKeyCollection = existingKeys.reduce((obj, key) => {
1262
1342
  const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]);
@@ -1285,7 +1365,8 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1285
1365
  const promises = [];
1286
1366
  // We need to get the previously existing values so we can compare the new ones
1287
1367
  // against them, to avoid unnecessary subscriber updates.
1288
- const previousCollectionPromise = Promise.all(existingKeys.map((key) => get(key).then((value) => [key, value]))).then(Object.fromEntries);
1368
+ // Include keys that will be removed so subscribers are notified about removals
1369
+ const previousCollectionPromise = Promise.all(allAffectedKeys.map((key) => get(key).then((value) => [key, value]))).then(Object.fromEntries);
1289
1370
  // New keys will be added via multiSet while existing keys will be updated using multiMerge
1290
1371
  // This is because setting a key that doesn't exist yet with multiMerge will throw errors
1291
1372
  if (keyValuePairsForExistingCollection.length > 0) {
@@ -1299,8 +1380,16 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1299
1380
  // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
1300
1381
  // and update all subscribers
1301
1382
  const promiseUpdate = previousCollectionPromise.then((previousCollection) => {
1383
+ // Capture the original cached values before merging
1384
+ const originalCachedValues = {};
1385
+ Object.keys(finalMergedCollection).forEach((key) => {
1386
+ originalCachedValues[key] = OnyxCache_1.default.get(key, false);
1387
+ });
1388
+ // Then merge all the data into cache as normal
1302
1389
  OnyxCache_1.default.merge(finalMergedCollection);
1303
- return scheduleNotifyCollectionSubscribers(collectionKey, finalMergedCollection, previousCollection);
1390
+ // Finally, preserve references for items that didn't actually change and include removed items as null
1391
+ const preservedCollection = preserveCollectionReferencesAfterMerge(finalMergedCollection, originalCachedValues, previousCollection);
1392
+ return scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
1304
1393
  });
1305
1394
  return Promise.all(promises)
1306
1395
  .catch((error) => retryOperation(error, mergeCollectionWithPatches, { collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate }, retryAttempt))
@@ -1351,8 +1440,9 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
1351
1440
  const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
1352
1441
  const previousCollection = getCachedCollection(collectionKey, existingKeys);
1353
1442
  const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
1354
- keyValuePairs.forEach(([key, value]) => OnyxCache_1.default.set(key, value));
1355
- const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
1443
+ // Preserve references for unchanged items and include removed items as null in partialSetCollection
1444
+ const preservedCollection = preserveCollectionReferences(keyValuePairs, previousCollection);
1445
+ const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
1356
1446
  return storage_1.default.multiSet(keyValuePairs)
1357
1447
  .catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt))
1358
1448
  .then(() => {
package/dist/useOnyx.js CHANGED
@@ -234,22 +234,24 @@ function useOnyx(key, options, dependencies = []) {
234
234
  if (shouldUpdateResult) {
235
235
  previousValueRef.current = newValueRef.current;
236
236
  // If the new value is `null` we default it to `undefined` to ensure the consumer gets a consistent result from the hook.
237
- const newStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded';
237
+ newFetchStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded';
238
238
  resultRef.current = [
239
239
  (_d = previousValueRef.current) !== null && _d !== void 0 ? _d : undefined,
240
240
  {
241
- status: newStatus,
241
+ status: newFetchStatus,
242
242
  sourceValue: sourceValueRef.current,
243
243
  },
244
244
  ];
245
245
  // If `canBeMissing` is set to `false` and the Onyx value of that key is not defined,
246
246
  // we log an alert so it can be acknowledged by the consumer. Additionally, we won't log alerts
247
247
  // if there's a `Onyx.clear()` task in progress.
248
- if ((options === null || options === void 0 ? void 0 : options.canBeMissing) === false && newStatus === 'loaded' && !isOnyxValueDefined && !OnyxCache_1.default.hasPendingTask(OnyxCache_1.TASK.CLEAR)) {
248
+ if ((options === null || options === void 0 ? void 0 : options.canBeMissing) === false && newFetchStatus === 'loaded' && !isOnyxValueDefined && !OnyxCache_1.default.hasPendingTask(OnyxCache_1.TASK.CLEAR)) {
249
249
  Logger.logAlert(`useOnyx returned no data for key with canBeMissing set to false for key ${key}`, { showAlert: true });
250
250
  }
251
251
  }
252
- OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current);
252
+ if (newFetchStatus !== 'loading') {
253
+ OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current);
254
+ }
253
255
  return resultRef.current;
254
256
  }, [options === null || options === void 0 ? void 0 : options.initWithStoredValues, options === null || options === void 0 ? void 0 : options.allowStaleData, options === null || options === void 0 ? void 0 : options.canBeMissing, key, memoizedSelector, cacheKey]);
255
257
  const subscribe = (0, react_1.useCallback)((onStoreChange) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.18",
3
+ "version": "3.0.20",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",