react-native-onyx 1.0.54 → 1.0.56

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.
@@ -268,6 +268,46 @@ function isSafeEvictionKey(testKey) {
268
268
  return underscore__WEBPACK_IMPORTED_MODULE_2___default().some(evictionAllowList, (key) => isKeyMatch(key, testKey));
269
269
  }
270
270
 
271
+ /**
272
+ * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
273
+ * If the requested key is a collection, it will return an object with all the collection members.
274
+ *
275
+ * @param {String} key
276
+ * @param {Object} mapping
277
+ * @returns {Mixed}
278
+ */
279
+ function tryGetCachedValue(key) {let mapping = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
280
+ let val = _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].getValue(key);
281
+
282
+ if (isCollectionKey(key)) {
283
+ const allKeys = _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].getAllKeys();
284
+ const matchingKeys = underscore__WEBPACK_IMPORTED_MODULE_2___default().filter(allKeys, (k) => k.startsWith(key));
285
+ const values = underscore__WEBPACK_IMPORTED_MODULE_2___default().reduce(matchingKeys, (finalObject, matchedKey) => {
286
+ const cachedValue = _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].getValue(matchedKey);
287
+ if (cachedValue) {
288
+ // This is permissible because we're in the process of constructing the final object in a reduce function.
289
+ // eslint-disable-next-line no-param-reassign
290
+ finalObject[matchedKey] = cachedValue;
291
+ }
292
+ return finalObject;
293
+ }, {});
294
+ if (underscore__WEBPACK_IMPORTED_MODULE_2___default().isEmpty(values)) {
295
+ return;
296
+ }
297
+ val = values;
298
+ }
299
+
300
+ if (mapping.selector) {
301
+ const state = mapping.withOnyxInstance ? mapping.withOnyxInstance.state : undefined;
302
+ if (isCollectionKey(key)) {
303
+ return reduceCollectionWithSelector(val, mapping.selector, state);
304
+ }
305
+ return getSubsetOfData(val, mapping.selector, state);
306
+ }
307
+
308
+ return val;
309
+ }
310
+
271
311
  /**
272
312
  * Remove a key from the recently accessed key list.
273
313
  *
@@ -932,19 +972,33 @@ function evictStorageAndRetry(error, onyxMethod) {for (var _len = arguments.leng
932
972
  *
933
973
  * @param {String} key
934
974
  * @param {*} value
975
+ * @param {Boolean} hasChanged
935
976
  * @param {String} method
936
977
  */
937
- function broadcastUpdate(key, value, method) {
978
+ function broadcastUpdate(key, value, hasChanged, method) {
938
979
  // Logging properties only since values could be sensitive things we don't want to log
939
980
  _Logger__WEBPACK_IMPORTED_MODULE_6__.logInfo(`${method}() called for key: ${key}${underscore__WEBPACK_IMPORTED_MODULE_2___default().isObject(value) ? ` properties: ${underscore__WEBPACK_IMPORTED_MODULE_2___default().keys(value).join(',')}` : ''}`);
940
981
 
941
982
  // Update subscribers if the cached value has changed, or when the subscriber specifically requires
942
983
  // all updates regardless of value changes (indicated by initWithStoredValues set to false).
943
- const hasChanged = _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].hasValueChanged(key, value);
944
- _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].set(key, value);
984
+ if (hasChanged) {
985
+ _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].set(key, value);
986
+ } else {
987
+ _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].addToAccessedKeys(key);
988
+ }
989
+
945
990
  notifySubscribersOnNextTick(key, value, (subscriber) => hasChanged || subscriber.initWithStoredValues === false);
946
991
  }
947
992
 
993
+ /**
994
+ * @private
995
+ * @param {String} key
996
+ * @returns {Boolean}
997
+ */
998
+ function hasPendingMergeForKey(key) {
999
+ return Boolean(mergeQueue[key]);
1000
+ }
1001
+
948
1002
  /**
949
1003
  * Write a value to our store with the given key
950
1004
  *
@@ -958,8 +1012,19 @@ function set(key, value) {
958
1012
  return remove(key);
959
1013
  }
960
1014
 
1015
+ if (hasPendingMergeForKey(key)) {
1016
+ _Logger__WEBPACK_IMPORTED_MODULE_6__.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
1017
+ }
1018
+
1019
+ const hasChanged = _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].hasValueChanged(key, value);
1020
+
961
1021
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
962
- broadcastUpdate(key, value, 'set');
1022
+ broadcastUpdate(key, value, hasChanged, 'set');
1023
+
1024
+ // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1025
+ if (!hasChanged) {
1026
+ return Promise.resolve();
1027
+ }
963
1028
 
964
1029
  return _storage__WEBPACK_IMPORTED_MODULE_5__["default"].setItem(key, value).
965
1030
  catch((error) => evictStorageAndRetry(error, set, key, value));
@@ -1002,11 +1067,11 @@ function multiSet(data) {
1002
1067
  * Merges an array of changes with an existing value
1003
1068
  *
1004
1069
  * @private
1005
- * @param {Array<*>} changes Array of changes that should be applied to the existing value
1006
1070
  * @param {*} existingValue
1071
+ * @param {Array<*>} changes Array of changes that should be applied to the existing value
1007
1072
  * @returns {*}
1008
1073
  */
1009
- function applyMerge(changes, existingValue) {
1074
+ function applyMerge(existingValue, changes) {
1010
1075
  const lastChange = underscore__WEBPACK_IMPORTED_MODULE_2___default().last(changes);
1011
1076
 
1012
1077
  if (underscore__WEBPACK_IMPORTED_MODULE_2___default().isArray(existingValue) || underscore__WEBPACK_IMPORTED_MODULE_2___default().isArray(lastChange)) {
@@ -1015,14 +1080,10 @@ function applyMerge(changes, existingValue) {
1015
1080
 
1016
1081
  if (underscore__WEBPACK_IMPORTED_MODULE_2___default().isObject(existingValue) || underscore__WEBPACK_IMPORTED_MODULE_2___default().every(changes, (underscore__WEBPACK_IMPORTED_MODULE_2___default().isObject))) {
1017
1082
  // Object values are merged one after the other
1018
- return underscore__WEBPACK_IMPORTED_MODULE_2___default().reduce(changes, (modifiedData, change) => {
1019
- // lodash adds a small overhead so we don't use it here
1020
- // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
1021
- const newData = Object.assign({}, (0,_fastMerge__WEBPACK_IMPORTED_MODULE_9__["default"])(modifiedData, change));
1022
-
1023
- // Remove all first level keys that are explicitly set to null.
1024
- return underscore__WEBPACK_IMPORTED_MODULE_2___default().omit(newData, (value) => underscore__WEBPACK_IMPORTED_MODULE_2___default().isNull(value));
1025
- }, existingValue || {});
1083
+ // lodash adds a small overhead so we don't use it here
1084
+ // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
1085
+ return underscore__WEBPACK_IMPORTED_MODULE_2___default().reduce(changes, (modifiedData, change) => (0,_fastMerge__WEBPACK_IMPORTED_MODULE_9__["default"])(modifiedData, change),
1086
+ existingValue || {});
1026
1087
  }
1027
1088
 
1028
1089
  // If we have anything else we can't merge it so we'll
@@ -1063,17 +1124,30 @@ function merge(key, changes) {
1063
1124
  then((existingValue) => {
1064
1125
  try {
1065
1126
  // We first only merge the changes, so we can provide these to the native implementation (SQLite uses only delta changes in "JSON_PATCH" to merge)
1066
- const batchedChanges = applyMerge(mergeQueue[key]);
1127
+ const batchedChanges = applyMerge(undefined, mergeQueue[key]);
1067
1128
 
1068
1129
  // Clean up the write queue so we
1069
1130
  // don't apply these changes again
1070
1131
  delete mergeQueue[key];
1071
1132
 
1072
1133
  // After that we merge the batched changes with the existing value
1073
- const modifiedData = applyMerge([batchedChanges], existingValue);
1134
+ let modifiedData = applyMerge(existingValue, [batchedChanges]);
1135
+
1136
+ // For objects, the key for null values needs to be removed from the object to ensure the value will get removed from storage completely.
1137
+ // On native, SQLite will remove top-level keys that are null. To be consistent, we remove them on web too.
1138
+ if (!underscore__WEBPACK_IMPORTED_MODULE_2___default().isArray(modifiedData) && underscore__WEBPACK_IMPORTED_MODULE_2___default().isObject(modifiedData)) {
1139
+ modifiedData = underscore__WEBPACK_IMPORTED_MODULE_2___default().omit(modifiedData, (value) => underscore__WEBPACK_IMPORTED_MODULE_2___default().isNull(value));
1140
+ }
1141
+
1142
+ const hasChanged = _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].hasValueChanged(key, modifiedData);
1074
1143
 
1075
1144
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
1076
- broadcastUpdate(key, modifiedData, 'merge');
1145
+ broadcastUpdate(key, modifiedData, hasChanged, 'merge');
1146
+
1147
+ // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1148
+ if (!hasChanged) {
1149
+ return Promise.resolve();
1150
+ }
1077
1151
 
1078
1152
  return _storage__WEBPACK_IMPORTED_MODULE_5__["default"].mergeItem(key, batchedChanges, modifiedData);
1079
1153
  } catch (error) {
@@ -1084,15 +1158,6 @@ function merge(key, changes) {
1084
1158
  });
1085
1159
  }
1086
1160
 
1087
- /**
1088
- * @private
1089
- * @param {String} key
1090
- * @returns {Boolean}
1091
- */
1092
- function hasPendingMergeForKey(key) {
1093
- return Boolean(mergeQueue[key]);
1094
- }
1095
-
1096
1161
  /**
1097
1162
  * Merge user provided default key value pairs.
1098
1163
  * @private
@@ -1411,7 +1476,8 @@ const Onyx = {
1411
1476
  removeFromEvictionBlockList,
1412
1477
  isSafeEvictionKey,
1413
1478
  METHOD,
1414
- setMemoryOnlyKeys
1479
+ setMemoryOnlyKeys,
1480
+ tryGetCachedValue
1415
1481
  };
1416
1482
 
1417
1483
  /**
@@ -2377,13 +2443,25 @@ function getDisplayName(component) {
2377
2443
  // disconnected. It is a key value store with the format {[mapping.key]: connectionID}.
2378
2444
  this.activeConnectionIDs = {};
2379
2445
 
2446
+ const cachedState = underscore__WEBPACK_IMPORTED_MODULE_2___default().reduce(mapOnyxToState, (resultObj, mapping, propertyName) => {
2447
+ const key = _Str__WEBPACK_IMPORTED_MODULE_3__.result(mapping.key, props);
2448
+ const value = _Onyx__WEBPACK_IMPORTED_MODULE_4__["default"].tryGetCachedValue(key, mapping);
2449
+
2450
+ if (value !== undefined) {
2451
+ // eslint-disable-next-line no-param-reassign
2452
+ resultObj[propertyName] = value;
2453
+ }
2454
+
2455
+ return resultObj;
2456
+ }, {});
2457
+
2458
+ // If we have all the data we need, then we can render the component immediately
2459
+ cachedState.loading = underscore__WEBPACK_IMPORTED_MODULE_2___default().size(cachedState) < requiredKeysForInit.length;
2460
+
2380
2461
  // Object holding the temporary initial state for the component while we load the various Onyx keys
2381
- this.tempState = {};
2462
+ this.tempState = cachedState;
2382
2463
 
2383
- this.state = {
2384
- // If there are no required keys for init then we can render the wrapped component immediately
2385
- loading: requiredKeysForInit.length > 0
2386
- };
2464
+ this.state = cachedState;
2387
2465
  }
2388
2466
 
2389
2467
  componentDidMount() {
@@ -2400,7 +2478,6 @@ function getDisplayName(component) {
2400
2478
  underscore__WEBPACK_IMPORTED_MODULE_2___default().each(mapOnyxToState, (mapping, propertyName) => {
2401
2479
  const previousKey = _Str__WEBPACK_IMPORTED_MODULE_3__.result(mapping.key, prevProps);
2402
2480
  const newKey = _Str__WEBPACK_IMPORTED_MODULE_3__.result(mapping.key, this.props);
2403
-
2404
2481
  if (previousKey !== newKey) {
2405
2482
  _Onyx__WEBPACK_IMPORTED_MODULE_4__["default"].disconnect(this.activeConnectionIDs[previousKey], previousKey);
2406
2483
  delete this.activeConnectionIDs[previousKey];
@@ -2428,6 +2505,16 @@ function getDisplayName(component) {
2428
2505
  * @param {*} val
2429
2506
  */
2430
2507
  setWithOnyxState(statePropertyName, val) {
2508
+ // We might have loaded the values for the onyx keys/mappings already from the cache.
2509
+ // In case we were able to load all the values upfront, the loading state will be false.
2510
+ // However, Onyx.js will always call setWithOnyxState, as it doesn't know that this implementation
2511
+ // already loaded the values from cache. Thus we have to check whether the value has changed
2512
+ // before we set the state to prevent unnecessary renders.
2513
+ const prevValue = this.state[statePropertyName];
2514
+ if (!this.state.loading && prevValue === val) {
2515
+ return;
2516
+ }
2517
+
2431
2518
  if (!this.state.loading) {
2432
2519
  this.setState({ [statePropertyName]: val });
2433
2520
  return;
@@ -2440,7 +2527,14 @@ function getDisplayName(component) {
2440
2527
  return;
2441
2528
  }
2442
2529
 
2443
- this.setState({ ...this.tempState, loading: false });
2530
+ const stateUpdate = { ...this.tempState, loading: false };
2531
+
2532
+ // The state is set here manually, instead of using setState because setState is an async operation, meaning it might execute on the next tick,
2533
+ // or at a later point in the microtask queue. That can lead to a race condition where
2534
+ // setWithOnyxState is called before the state is actually set. This results in unreliable behavior when checking the loading state and has been mainly observed on fabric.
2535
+ this.state = stateUpdate;
2536
+
2537
+ this.setState(stateUpdate); // Trigger a render
2444
2538
  delete this.tempState;
2445
2539
  }
2446
2540