react-native-onyx 1.0.24 → 1.0.26

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/lib/Onyx.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-continue */
2
2
  import _ from 'underscore';
3
3
  import Str from 'expensify-common/lib/str';
4
+ import {deepEqual} from 'fast-equals';
4
5
  import lodashGet from 'lodash/get';
5
6
  import Storage from './storage';
6
7
  import * as Logger from './Logger';
@@ -35,6 +36,33 @@ let defaultKeyStates = {};
35
36
  // Connections can be made before `Onyx.init`. They would wait for this task before resolving
36
37
  const deferredInitTask = createDeferredTask();
37
38
 
39
+ /**
40
+ * Uses a selector string or function to return a simplified version of sourceData
41
+ * @param {Mixed} sourceData
42
+ * @param {String|Function} selector
43
+ * If it's a string, the selector is passed to lodashGet on the sourceData
44
+ * If it's a function, it is passed the sourceData and it should return the simplified data
45
+ * @returns {Mixed}
46
+ */
47
+ const getSubsetOfData = (sourceData, selector) => (_.isFunction(selector)
48
+ ? selector(sourceData)
49
+ : lodashGet(sourceData, selector));
50
+
51
+ /**
52
+ * Takes a collection of items (eg. {testKey_1:{a:'a'}, testKey_2:{b:'b'}})
53
+ * and runs it through a reducer function to return a subset of the data according to a selector.
54
+ * The resulting collection will only contain items that are returned by the selector.
55
+ * @param {Object} collection
56
+ * @param {String|Function} selector (see method docs for getSubsetOfData() for full details)
57
+ * @returns {Object}
58
+ */
59
+ const reduceCollectionWithSelector = (collection, selector) => _.reduce(collection, (finalCollection, item, key) => {
60
+ // eslint-disable-next-line no-param-reassign
61
+ finalCollection[key] = getSubsetOfData(item, selector);
62
+
63
+ return finalCollection;
64
+ }, {});
65
+
38
66
  /**
39
67
  * Get some data from the store
40
68
  *
@@ -322,6 +350,23 @@ function keysChanged(collectionKey, partialCollection) {
322
350
  // We are subscribed to a collection key so we must update the data in state with the new
323
351
  // collection member key values from the partial update.
324
352
  if (isSubscribedToCollectionKey) {
353
+ // If the subscriber has a selector, then the component's state must only be updated with the data
354
+ // returned by the selector.
355
+ if (subscriber.selector) {
356
+ subscriber.withOnyxInstance.setState((prevState) => {
357
+ const previousData = reduceCollectionWithSelector(prevState[subscriber.statePropertyName], subscriber.selector);
358
+ const newData = reduceCollectionWithSelector(cachedCollection, subscriber.selector);
359
+
360
+ if (!deepEqual(previousData, newData)) {
361
+ return {
362
+ [subscriber.statePropertyName]: newData,
363
+ };
364
+ }
365
+ return null;
366
+ });
367
+ continue;
368
+ }
369
+
325
370
  subscriber.withOnyxInstance.setState((prevState) => {
326
371
  const finalCollection = _.clone(prevState[subscriber.statePropertyName] || {});
327
372
  const dataKeys = _.keys(partialCollection);
@@ -347,6 +392,25 @@ function keysChanged(collectionKey, partialCollection) {
347
392
  continue;
348
393
  }
349
394
 
395
+ // If the subscriber has a selector, then the component's state must only be updated with the data
396
+ // returned by the selector and the state should only change when the subset of data changes from what
397
+ // it was previously.
398
+ if (subscriber.selector) {
399
+ subscriber.withOnyxInstance.setState((prevState) => {
400
+ const prevData = prevState[subscriber.statePropertyName];
401
+ const newData = getSubsetOfData(cachedCollection[subscriber.key], subscriber.selector);
402
+ if (!deepEqual(prevData, newData)) {
403
+ PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keysChanged', collectionKey);
404
+ return {
405
+ [subscriber.statePropertyName]: newData,
406
+ };
407
+ }
408
+
409
+ return null;
410
+ });
411
+ continue;
412
+ }
413
+
350
414
  subscriber.withOnyxInstance.setState((prevState) => {
351
415
  const data = cachedCollection[subscriber.key];
352
416
  const previousData = prevState[subscriber.statePropertyName];
@@ -410,6 +474,29 @@ function keyChanged(key, data, canUpdateSubscriber) {
410
474
  if (subscriber.withOnyxInstance) {
411
475
  // Check if we are subscribing to a collection key and overwrite the collection member key value in state
412
476
  if (isCollectionKey(subscriber.key)) {
477
+ // If the subscriber has a selector, then the consumer of this data must only be given the data
478
+ // returned by the selector and only when the selected data has changed.
479
+ if (subscriber.selector) {
480
+ subscriber.withOnyxInstance.setState((prevState) => {
481
+ const prevData = prevState[subscriber.statePropertyName];
482
+ const newData = {
483
+ [key]: getSubsetOfData(data, subscriber.selector),
484
+ };
485
+ const prevDataWithNewData = {
486
+ ...prevData,
487
+ [key]: getSubsetOfData(data, subscriber.selector),
488
+ };
489
+ if (!deepEqual(prevData, prevDataWithNewData)) {
490
+ PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keyChanged', key);
491
+ return {
492
+ [subscriber.statePropertyName]: prevDataWithNewData,
493
+ };
494
+ }
495
+ return null;
496
+ });
497
+ continue;
498
+ }
499
+
413
500
  subscriber.withOnyxInstance.setState((prevState) => {
414
501
  const collection = prevState[subscriber.statePropertyName] || {};
415
502
  const newCollection = {
@@ -424,6 +511,22 @@ function keyChanged(key, data, canUpdateSubscriber) {
424
511
  continue;
425
512
  }
426
513
 
514
+ // If the subscriber has a selector, then the component's state must only be updated with the data
515
+ // returned by the selector and only if the selected data has changed.
516
+ if (subscriber.selector) {
517
+ subscriber.withOnyxInstance.setState((prevState) => {
518
+ const previousValue = getSubsetOfData(prevState, subscriber.selector);
519
+ const newValue = getSubsetOfData(data, subscriber.selector);
520
+ if (!deepEqual(previousValue, newValue)) {
521
+ return {
522
+ [subscriber.statePropertyName]: newValue,
523
+ };
524
+ }
525
+ return null;
526
+ });
527
+ continue;
528
+ }
529
+
427
530
  // If we did not match on a collection key then we just set the new data to the state property
428
531
  subscriber.withOnyxInstance.setState((prevState) => {
429
532
  const previousData = prevState[subscriber.statePropertyName];
@@ -449,25 +552,41 @@ function keyChanged(key, data, canUpdateSubscriber) {
449
552
  * - triggers the callback function
450
553
  *
451
554
  * @private
452
- * @param {object} config
453
- * @param {object} [config.withOnyxInstance]
454
- * @param {string} [config.statePropertyName]
455
- * @param {function} [config.callback]
555
+ * @param {Object} mapping
556
+ * @param {Object} [mapping.withOnyxInstance]
557
+ * @param {String} [mapping.statePropertyName]
558
+ * @param {Function} [mapping.callback]
559
+ * @param {String} [mapping.selector]
456
560
  * @param {*|null} val
457
561
  * @param {String} matchedKey
458
562
  */
459
- function sendDataToConnection(config, val, matchedKey) {
563
+ function sendDataToConnection(mapping, val, matchedKey) {
460
564
  // If the mapping no longer exists then we should not send any data.
461
565
  // This means our subscriber disconnected or withOnyx wrapped component unmounted.
462
- if (!callbackToStateMapping[config.connectionID]) {
566
+ if (!callbackToStateMapping[mapping.connectionID]) {
463
567
  return;
464
568
  }
465
569
 
466
- if (config.withOnyxInstance) {
467
- PerformanceUtils.logSetStateCall(config, null, val, 'sendDataToConnection');
468
- config.withOnyxInstance.setWithOnyxState(config.statePropertyName, val);
469
- } else if (_.isFunction(config.callback)) {
470
- config.callback(val, matchedKey);
570
+ if (mapping.withOnyxInstance) {
571
+ let newData = val;
572
+
573
+ // If the mapping has a selector, then the component's state must only be updated with the data
574
+ // returned by the selector.
575
+ if (mapping.selector) {
576
+ if (isCollectionKey(mapping.key)) {
577
+ newData = reduceCollectionWithSelector(val, mapping.selector);
578
+ } else {
579
+ newData = getSubsetOfData(val, mapping.selector);
580
+ }
581
+ }
582
+
583
+ PerformanceUtils.logSetStateCall(mapping, null, newData, 'sendDataToConnection');
584
+ mapping.withOnyxInstance.setWithOnyxState(mapping.statePropertyName, newData);
585
+ return;
586
+ }
587
+
588
+ if (_.isFunction(mapping.callback)) {
589
+ mapping.callback(val, matchedKey);
471
590
  }
472
591
  }
473
592
 
@@ -534,6 +653,7 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
534
653
  * @param {Boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the
535
654
  * component
536
655
  * @param {Boolean} [mapping.waitForCollectionCallback] If set to true, it will return the entire collection to the callback as a single object
656
+ * @param {String|Function} [mapping.selector] THIS PARAM IS ONLY USED WITH withOnyx(). If included, this will be used to subscribe to a subset of an Onyx key's data. If the selector is a string, the selector is passed to lodashGet on the sourceData. If the selector is a function, the sourceData is passed to the selector and should return the simplified data. Using this setting on `withOnyx` can have very positive performance benefits because the component will only re-render when the subset of data changes. Otherwise, any change of data on any property would normally cause the component to re-render (and that can be expensive from a performance standpoint).
537
657
  * @returns {Number} an ID to use when calling disconnect
538
658
  */
539
659
  function connect(mapping) {
@@ -926,6 +1046,11 @@ function clear() {
926
1046
  * @returns {Promise}
927
1047
  */
928
1048
  function mergeCollection(collectionKey, collection) {
1049
+ if (!_.isObject(collection) || _.isArray(collection) || _.isEmpty(collection)) {
1050
+ Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
1051
+ return Promise.resolve();
1052
+ }
1053
+
929
1054
  // Confirm all the collection keys belong to the same parent
930
1055
  _.each(collection, (_data, dataKey) => {
931
1056
  if (isKeyMatch(collectionKey, dataKey)) {
package/lib/OnyxCache.js CHANGED
@@ -111,6 +111,10 @@ class OnyxCache {
111
111
  * @param {Record<string, *>} data - a map of (cache) key - values
112
112
  */
113
113
  merge(data) {
114
+ if (!_.isObject(data) || _.isArray(data)) {
115
+ throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
116
+ }
117
+
114
118
  // lodash adds a small overhead so we don't use it here
115
119
  // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
116
120
  this.storageMap = Object.assign({}, fastMerge(this.storageMap, data));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",