react-native-onyx 1.0.81 → 1.0.83

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
@@ -5,10 +5,9 @@ import * as Logger from './Logger';
5
5
  import cache from './OnyxCache';
6
6
  import * as Str from './Str';
7
7
  import createDeferredTask from './createDeferredTask';
8
- import fastMerge from './fastMerge';
9
8
  import * as PerformanceUtils from './metrics/PerformanceUtils';
10
9
  import Storage from './storage';
11
- import Utils from './utils';
10
+ import utils from './utils';
12
11
  import unstable_batchedUpdates from './batch';
13
12
 
14
13
  // Method constants
@@ -415,7 +414,7 @@ function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers =
415
414
  // If the subscriber has a selector, then the component's state must only be updated with the data
416
415
  // returned by the selector.
417
416
  if (subscriber.selector) {
418
- subscriber.withOnyxInstance.setState((prevState) => {
417
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
419
418
  const previousData = prevState[subscriber.statePropertyName];
420
419
  const newData = reduceCollectionWithSelector(cachedCollection, subscriber.selector, subscriber.withOnyxInstance.state);
421
420
 
@@ -429,7 +428,7 @@ function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers =
429
428
  continue;
430
429
  }
431
430
 
432
- subscriber.withOnyxInstance.setState((prevState) => {
431
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
433
432
  const finalCollection = _.clone(prevState[subscriber.statePropertyName] || {});
434
433
  const dataKeys = _.keys(partialCollection);
435
434
  for (let j = 0; j < dataKeys.length; j++) {
@@ -458,7 +457,7 @@ function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers =
458
457
  // returned by the selector and the state should only change when the subset of data changes from what
459
458
  // it was previously.
460
459
  if (subscriber.selector) {
461
- subscriber.withOnyxInstance.setState((prevState) => {
460
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
462
461
  const prevData = prevState[subscriber.statePropertyName];
463
462
  const newData = getSubsetOfData(cachedCollection[subscriber.key], subscriber.selector, subscriber.withOnyxInstance.state);
464
463
  if (!deepEqual(prevData, newData)) {
@@ -473,9 +472,14 @@ function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers =
473
472
  continue;
474
473
  }
475
474
 
476
- subscriber.withOnyxInstance.setState((prevState) => {
475
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
477
476
  const data = cachedCollection[subscriber.key];
478
477
  const previousData = prevState[subscriber.statePropertyName];
478
+
479
+ // Avoids triggering unnecessary re-renders when feeding empty objects
480
+ if (utils.areObjectsEmpty(data, previousData)) {
481
+ return null;
482
+ }
479
483
  if (data === previousData) {
480
484
  return null;
481
485
  }
@@ -548,7 +552,7 @@ function keyChanged(key, data, canUpdateSubscriber, notifyRegularSubscibers = tr
548
552
  // If the subscriber has a selector, then the consumer of this data must only be given the data
549
553
  // returned by the selector and only when the selected data has changed.
550
554
  if (subscriber.selector) {
551
- subscriber.withOnyxInstance.setState((prevState) => {
555
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
552
556
  const prevData = prevState[subscriber.statePropertyName];
553
557
  const newData = {
554
558
  [key]: getSubsetOfData(data, subscriber.selector, subscriber.withOnyxInstance.state),
@@ -568,7 +572,7 @@ function keyChanged(key, data, canUpdateSubscriber, notifyRegularSubscibers = tr
568
572
  continue;
569
573
  }
570
574
 
571
- subscriber.withOnyxInstance.setState((prevState) => {
575
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
572
576
  const collection = prevState[subscriber.statePropertyName] || {};
573
577
  const newCollection = {
574
578
  ...collection,
@@ -585,7 +589,7 @@ function keyChanged(key, data, canUpdateSubscriber, notifyRegularSubscibers = tr
585
589
  // If the subscriber has a selector, then the component's state must only be updated with the data
586
590
  // returned by the selector and only if the selected data has changed.
587
591
  if (subscriber.selector) {
588
- subscriber.withOnyxInstance.setState((prevState) => {
592
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
589
593
  const previousValue = getSubsetOfData(prevState[subscriber.statePropertyName], subscriber.selector, subscriber.withOnyxInstance.state);
590
594
  const newValue = getSubsetOfData(data, subscriber.selector, subscriber.withOnyxInstance.state);
591
595
  if (!deepEqual(previousValue, newValue)) {
@@ -599,8 +603,13 @@ function keyChanged(key, data, canUpdateSubscriber, notifyRegularSubscibers = tr
599
603
  }
600
604
 
601
605
  // If we did not match on a collection key then we just set the new data to the state property
602
- subscriber.withOnyxInstance.setState((prevState) => {
606
+ subscriber.withOnyxInstance.setStateProxy((prevState) => {
603
607
  const previousData = prevState[subscriber.statePropertyName];
608
+
609
+ // Avoids triggering unnecessary re-renders when feeding empty objects
610
+ if (utils.areObjectsEmpty(data, previousData)) {
611
+ return null;
612
+ }
604
613
  if (previousData === data) {
605
614
  return null;
606
615
  }
@@ -728,6 +737,9 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
728
737
  * The sourceData and withOnyx state are passed to the selector and should return the simplified data. Using this setting on `withOnyx` can have very positive
729
738
  * 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
730
739
  * cause the component to re-render (and that can be expensive from a performance standpoint).
740
+ * @param {String | Number | Boolean | Object} [mapping.initialValue] THIS PARAM IS ONLY USED WITH withOnyx().
741
+ * If included, this will be passed to the component so that something can be rendered while data is being fetched from the DB.
742
+ * Note that it will not cause the component to have the loading prop set to true. |
731
743
  * @returns {Number} an ID to use when calling disconnect
732
744
  */
733
745
  function connect(mapping) {
@@ -1008,7 +1020,7 @@ function set(key, value) {
1008
1020
  Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
1009
1021
  }
1010
1022
 
1011
- const valueWithNullRemoved = Utils.removeNullObjectValues(value);
1023
+ const valueWithNullRemoved = utils.removeNullObjectValues(value);
1012
1024
 
1013
1025
  const hasChanged = cache.hasValueChanged(key, valueWithNullRemoved);
1014
1026
 
@@ -1078,7 +1090,7 @@ function applyMerge(existingValue, changes) {
1078
1090
  // Object values are merged one after the other
1079
1091
  // lodash adds a small overhead so we don't use it here
1080
1092
  // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
1081
- return _.reduce(changes, (modifiedData, change) => fastMerge(modifiedData, change),
1093
+ return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change),
1082
1094
  existingValue || {});
1083
1095
  }
1084
1096
 
@@ -1127,14 +1139,14 @@ function merge(key, changes) {
1127
1139
  delete mergeQueuePromise[key];
1128
1140
 
1129
1141
  // After that we merge the batched changes with the existing value
1130
- const modifiedData = Utils.removeNullObjectValues(applyMerge(existingValue, [batchedChanges]));
1142
+ const modifiedData = utils.removeNullObjectValues(applyMerge(existingValue, [batchedChanges]));
1131
1143
 
1132
1144
  // On native platforms we use SQLite which utilises JSON_PATCH to merge changes.
1133
1145
  // JSON_PATCH generally removes top-level nullish values from the stored object.
1134
1146
  // When there is no existing value though, SQLite will just insert the changes as a new value and thus the top-level nullish values won't be removed.
1135
1147
  // Therefore we need to remove nullish values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
1136
1148
  if (!existingValue) {
1137
- batchedChanges = Utils.removeNullObjectValues(batchedChanges);
1149
+ batchedChanges = utils.removeNullObjectValues(batchedChanges);
1138
1150
  }
1139
1151
 
1140
1152
  const hasChanged = cache.hasValueChanged(key, modifiedData);
@@ -1168,7 +1180,7 @@ function initializeWithDefaultKeyStates() {
1168
1180
  .then((pairs) => {
1169
1181
  const asObject = _.object(pairs);
1170
1182
 
1171
- const merged = fastMerge(asObject, defaultKeyStates);
1183
+ const merged = utils.fastMerge(asObject, defaultKeyStates);
1172
1184
  cache.merge(merged);
1173
1185
  _.each(merged, (val, key) => keyChanged(key, val));
1174
1186
  });
package/lib/OnyxCache.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import _ from 'underscore';
2
2
  import {deepEqual} from 'fast-equals';
3
- import fastMerge from './fastMerge';
3
+ import utils from './utils';
4
4
 
5
5
  const isDefined = _.negate(_.isUndefined);
6
6
 
@@ -119,7 +119,7 @@ class OnyxCache {
119
119
 
120
120
  // lodash adds a small overhead so we don't use it here
121
121
  // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
122
- this.storageMap = Object.assign({}, fastMerge(this.storageMap, data));
122
+ this.storageMap = Object.assign({}, utils.fastMerge(this.storageMap, data));
123
123
 
124
124
  const storageKeys = this.getAllKeys();
125
125
  const mergedKeys = _.keys(data);
@@ -1,5 +1,5 @@
1
1
  import _ from 'underscore';
2
- import fastMerge from '../../fastMerge';
2
+ import utils from '../../utils';
3
3
 
4
4
  let storageMapInternal = {};
5
5
 
@@ -27,7 +27,7 @@ const idbKeyvalMock = {
27
27
  _.forEach(pairs, ([key, value]) => {
28
28
  const existingValue = storageMapInternal[key];
29
29
  const newValue = _.isObject(existingValue)
30
- ? fastMerge(existingValue, value) : value;
30
+ ? utils.fastMerge(existingValue, value) : value;
31
31
 
32
32
  set(key, newValue);
33
33
  });
@@ -11,8 +11,7 @@ import {
11
11
  promisifyRequest,
12
12
  } from 'idb-keyval';
13
13
  import _ from 'underscore';
14
- import fastMerge from '../../fastMerge';
15
- import Utils from '../../utils';
14
+ import utils from '../../utils';
16
15
 
17
16
  // We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB
18
17
  // which might not be available in certain environments that load the bundle (e.g. electron main process).
@@ -56,8 +55,8 @@ const provider = {
56
55
  return getValues.then((values) => {
57
56
  const upsertMany = _.map(pairs, ([key, value], index) => {
58
57
  const prev = values[index];
59
- const newValue = _.isObject(prev) ? fastMerge(prev, value) : value;
60
- return promisifyRequest(store.put(Utils.removeNullObjectValues(newValue), key));
58
+ const newValue = _.isObject(prev) ? utils.fastMerge(prev, value) : value;
59
+ return promisifyRequest(store.put(utils.removeNullObjectValues(newValue), key));
61
60
  });
62
61
  return Promise.all(upsertMany);
63
62
  });
package/lib/utils.js CHANGED
@@ -1,4 +1,77 @@
1
- import _ from 'underscore';
1
+ import * as _ from 'underscore';
2
+
3
+ function areObjectsEmpty(a, b) {
4
+ return (
5
+ typeof a === 'object'
6
+ && typeof b === 'object'
7
+ && _.isEmpty(a)
8
+ && _.isEmpty(b)
9
+ );
10
+ }
11
+
12
+ // Mostly copied from https://medium.com/@lubaka.a/how-to-remove-lodash-performance-improvement-b306669ad0e1
13
+
14
+ /**
15
+ * @param {mixed} val
16
+ * @returns {boolean}
17
+ */
18
+ function isMergeableObject(val) {
19
+ const nonNullObject = val != null ? typeof val === 'object' : false;
20
+ return (nonNullObject
21
+ && Object.prototype.toString.call(val) !== '[object RegExp]'
22
+ && Object.prototype.toString.call(val) !== '[object Date]');
23
+ }
24
+
25
+ /**
26
+ * @param {Object} target
27
+ * @param {Object} source
28
+ * @returns {Object}
29
+ */
30
+ function mergeObject(target, source) {
31
+ const destination = {};
32
+ if (isMergeableObject(target)) {
33
+ // lodash adds a small overhead so we don't use it here
34
+ // eslint-disable-next-line rulesdir/prefer-underscore-method
35
+ const targetKeys = Object.keys(target);
36
+ for (let i = 0; i < targetKeys.length; ++i) {
37
+ const key = targetKeys[i];
38
+ destination[key] = target[key];
39
+ }
40
+ }
41
+
42
+ // lodash adds a small overhead so we don't use it here
43
+ // eslint-disable-next-line rulesdir/prefer-underscore-method
44
+ const sourceKeys = Object.keys(source);
45
+ for (let i = 0; i < sourceKeys.length; ++i) {
46
+ const key = sourceKeys[i];
47
+ if (source[key] === undefined) {
48
+ // eslint-disable-next-line no-continue
49
+ continue;
50
+ }
51
+ if (!isMergeableObject(source[key]) || !target[key]) {
52
+ destination[key] = source[key];
53
+ } else {
54
+ // eslint-disable-next-line no-use-before-define
55
+ destination[key] = fastMerge(target[key], source[key]);
56
+ }
57
+ }
58
+
59
+ return destination;
60
+ }
61
+
62
+ /**
63
+ * @param {Object|Array} target
64
+ * @param {Object|Array} source
65
+ * @returns {Object|Array}
66
+ */
67
+ function fastMerge(target, source) {
68
+ // lodash adds a small overhead so we don't use it here
69
+ // eslint-disable-next-line rulesdir/prefer-underscore-method
70
+ if (_.isArray(source) || _.isNull(source) || _.isUndefined(source)) {
71
+ return source;
72
+ }
73
+ return mergeObject(target, source);
74
+ }
2
75
 
3
76
  /**
4
77
  * We generally want to remove top-level nullish values from objects written to disk and cache, because it decreases the amount of data stored in memory and on disk.
@@ -20,4 +93,5 @@ function removeNullObjectValues(value) {
20
93
  return objectWithoutNullObjectValues;
21
94
  }
22
95
 
23
- export default {removeNullObjectValues};
96
+ export default {removeNullObjectValues, areObjectsEmpty, fastMerge};
97
+
package/lib/withOnyx.d.ts CHANGED
@@ -148,6 +148,7 @@ declare function withOnyx<TComponentProps, TOnyxProps>(
148
148
  | OnyxPropMapping<TComponentProps, TOnyxProps, TOnyxProp>
149
149
  | OnyxPropCollectionMapping<TComponentProps, TOnyxProps, TOnyxProp>;
150
150
  },
151
+ shouldDelayUpdates?: boolean,
151
152
  ): (component: React.ComponentType<TComponentProps>) => React.ComponentType<Omit<TComponentProps, keyof TOnyxProps>>;
152
153
 
153
154
  export default withOnyx;
package/lib/withOnyx.js CHANGED
@@ -8,6 +8,7 @@ import React from 'react';
8
8
  import _ from 'underscore';
9
9
  import Onyx from './Onyx';
10
10
  import * as Str from './Str';
11
+ import utils from './utils';
11
12
 
12
13
  /**
13
14
  * Returns the display name of a component
@@ -19,7 +20,7 @@ function getDisplayName(component) {
19
20
  return component.displayName || component.name || 'Component';
20
21
  }
21
22
 
22
- export default function (mapOnyxToState) {
23
+ export default function (mapOnyxToState, shouldDelayUpdates = false) {
23
24
  // A list of keys that must be present in tempState before we can render the WrappedComponent
24
25
  const requiredKeysForInit = _.chain(mapOnyxToState)
25
26
  .omit(config => config.initWithStoredValues === false)
@@ -28,37 +29,51 @@ export default function (mapOnyxToState) {
28
29
  return (WrappedComponent) => {
29
30
  const displayName = getDisplayName(WrappedComponent);
30
31
  class withOnyx extends React.Component {
32
+ pendingSetStates = [];
33
+
31
34
  constructor(props) {
32
35
  super(props);
33
-
36
+ this.shouldDelayUpdates = shouldDelayUpdates;
34
37
  this.setWithOnyxState = this.setWithOnyxState.bind(this);
38
+ this.flushPendingSetStates = this.flushPendingSetStates.bind(this);
35
39
 
36
40
  // This stores all the Onyx connection IDs to be used when the component unmounts so everything can be
37
41
  // disconnected. It is a key value store with the format {[mapping.key]: connectionID}.
38
42
  this.activeConnectionIDs = {};
39
43
 
40
- const cachedState = _.reduce(mapOnyxToState, (resultObj, mapping, propertyName) => {
41
- const key = Str.result(mapping.key, props);
42
- const value = Onyx.tryGetCachedValue(key, mapping);
43
-
44
- /**
45
- * If we have a pending merge for a key it could mean that data is being set via Onyx.merge() and someone expects a component to have this data immediately.
46
- *
47
- * @example
48
- *
49
- * Onyx.merge('report_123', value);
50
- * Navigation.navigate(route); // Where "route" expects the "value" to be available immediately once rendered.
51
- *
52
- * In reality, Onyx.merge() will only update the subscriber after all merges have been batched and the previous value is retrieved via a get() (returns a promise).
53
- * So, we won't use the cache optimization here as it will lead us to arbitrarily defer various actions in the application code.
54
- */
55
- if (value !== undefined && !Onyx.hasPendingMergeForKey(key)) {
56
- // eslint-disable-next-line no-param-reassign
57
- resultObj[propertyName] = value;
58
- }
44
+ const cachedState = _.reduce(
45
+ mapOnyxToState,
46
+ (resultObj, mapping, propertyName) => {
47
+ const key = Str.result(mapping.key, props);
48
+ let value = Onyx.tryGetCachedValue(key, mapping);
49
+ if (!value && mapping.initialValue) {
50
+ value = mapping.initialValue;
51
+ }
59
52
 
60
- return resultObj;
61
- }, {});
53
+ /**
54
+ * If we have a pending merge for a key it could mean that data is being set via Onyx.merge() and someone expects a component to have this data immediately.
55
+ *
56
+ * @example
57
+ *
58
+ * Onyx.merge('report_123', value);
59
+ * Navigation.navigate(route); // Where "route" expects the "value" to be available immediately once rendered.
60
+ *
61
+ * In reality, Onyx.merge() will only update the subscriber after all merges have been batched and the previous value is retrieved via a get() (returns a promise).
62
+ * So, we won't use the cache optimization here as it will lead us to arbitrarily defer various actions in the application code.
63
+ */
64
+ if (
65
+ (value !== undefined
66
+ && !Onyx.hasPendingMergeForKey(key))
67
+ || mapping.allowStaleData
68
+ ) {
69
+ // eslint-disable-next-line no-param-reassign
70
+ resultObj[propertyName] = value;
71
+ }
72
+
73
+ return resultObj;
74
+ },
75
+ {},
76
+ );
62
77
 
63
78
  // If we have all the data we need, then we can render the component immediately
64
79
  cachedState.loading = _.size(cachedState) < requiredKeysForInit.length;
@@ -101,47 +116,86 @@ export default function (mapOnyxToState) {
101
116
  });
102
117
  }
103
118
 
119
+ setStateProxy(modifier) {
120
+ if (this.shouldDelayUpdates) {
121
+ this.pendingSetStates.push(modifier);
122
+ } else {
123
+ this.setState(modifier);
124
+ }
125
+ }
126
+
104
127
  /**
105
- * This method is used externally by sendDataToConnection to prevent unnecessary renders while a component
106
- * still in a loading state. The temporary initial state is saved to the component instance and setState()
128
+ * This method is used by the internal raw Onyx `sendDataToConnection`, it is designed to prevent unnecessary renders while a component
129
+ * still in a "loading" (read "mounting") state. The temporary initial state is saved to the HOC instance and setState()
107
130
  * only called once all the necessary data has been collected.
108
131
  *
132
+ * There is however the possibility the component could have been updated by a call to setState()
133
+ * before the data was "initially" collected. A race condition.
134
+ * For example some update happened on some key, while onyx was still gathering the initial hydration data.
135
+ * This update is disptached directly to setStateProxy and therefore the component has the most up-to-date data
136
+ *
137
+ * This is a design flaw in Onyx itself as dispatching updates before initial hydration is not a correct event flow.
138
+ * We however need to workaround this issue in the HOC. The addition of initialValue makes things even more complex,
139
+ * since you cannot be really sure if the component has been updated before or after the initial hydration. Therefore if
140
+ * initialValue is there, we just check if the update is different than that and then try to handle it as best as we can.
141
+ *
109
142
  * @param {String} statePropertyName
110
143
  * @param {*} val
111
144
  */
112
145
  setWithOnyxState(statePropertyName, val) {
113
- // We might have loaded the values for the onyx keys/mappings already from the cache.
114
- // In case we were able to load all the values upfront, the loading state will be false.
115
- // However, Onyx.js will always call setWithOnyxState, as it doesn't know that this implementation
116
- // already loaded the values from cache. Thus we have to check whether the value has changed
117
- // before we set the state to prevent unnecessary renders.
118
146
  const prevValue = this.state[statePropertyName];
119
- if (!this.state.loading && prevValue === val) {
120
- return;
121
- }
122
147
 
148
+ // If the component is not loading (read "mounting"), then we can just update the state
123
149
  if (!this.state.loading) {
124
- this.setState({[statePropertyName]: val});
150
+ // Performance optimization, do not trigger update with same values
151
+ if (prevValue === val || utils.areObjectsEmpty(prevValue, val)) {
152
+ return;
153
+ }
154
+
155
+ this.setStateProxy({[statePropertyName]: val});
125
156
  return;
126
157
  }
127
158
 
128
159
  this.tempState[statePropertyName] = val;
129
160
 
130
- // All state keys should exist and at least have a value of null
131
- if (_.some(requiredKeysForInit, key => _.isUndefined(this.tempState[key]))) {
161
+ // If some key does not have a value yet, do not update the state yet
162
+ const tempStateIsMissingKey = _.some(requiredKeysForInit, key => _.isUndefined(this.tempState[key]));
163
+ if (tempStateIsMissingKey) {
132
164
  return;
133
165
  }
134
166
 
135
- // Leave untouched previous state to avoid data loss during pre-load updates.
136
- // This handles case when setState was called before the setWithOnyxState.
137
- // For example, when an Onyx property was updated by keyChanged before the call of the setWithOnyxState.
167
+ const stateUpdate = {...this.tempState};
168
+ delete this.tempState;
169
+
170
+ // Full of hacky workarounds to prevent the race condition described above.
138
171
  this.setState((prevState) => {
139
- const remainingTempState = _.omit(this.tempState, _.keys(prevState));
172
+ const finalState = _.reduce(stateUpdate, (result, value, key) => {
173
+ if (key === 'loading') {
174
+ return result;
175
+ }
140
176
 
141
- return ({...remainingTempState, loading: false});
142
- });
177
+ const initialValue = mapOnyxToState[key].initialValue;
143
178
 
144
- delete this.tempState;
179
+ // If initialValue is there and the state contains something different it means
180
+ // an update has already been received and we can discard the value we are trying to hydrate
181
+ if (!_.isUndefined(initialValue) && !_.isUndefined(prevState[key]) && prevState[key] !== initialValue) {
182
+ // eslint-disable-next-line no-param-reassign
183
+ result[key] = prevState[key];
184
+
185
+ // if value is already there (without initial value) then we can discard the value we are trying to hydrate
186
+ } else if (!_.isUndefined(prevState[key])) {
187
+ // eslint-disable-next-line no-param-reassign
188
+ result[key] = prevState[key];
189
+ } else {
190
+ // eslint-disable-next-line no-param-reassign
191
+ result[key] = value;
192
+ }
193
+ return result;
194
+ }, {});
195
+
196
+ finalState.loading = false;
197
+ return finalState;
198
+ });
145
199
  }
146
200
 
147
201
  /**
@@ -196,7 +250,23 @@ export default function (mapOnyxToState) {
196
250
  });
197
251
  }
198
252
 
253
+ flushPendingSetStates() {
254
+ if (!this.shouldDelayUpdates) {
255
+ return;
256
+ }
257
+
258
+ this.shouldDelayUpdates = false;
259
+
260
+ this.pendingSetStates.forEach((modifier) => {
261
+ this.setState(modifier);
262
+ });
263
+ this.pendingSetStates = [];
264
+ }
265
+
199
266
  render() {
267
+ // Remove any null values so that React replaces them with default props
268
+ const propsToPass = _.omit(this.props, _.isNull);
269
+
200
270
  if (this.state.loading) {
201
271
  return null;
202
272
  }
@@ -204,14 +274,12 @@ export default function (mapOnyxToState) {
204
274
  // Remove any internal state properties used by withOnyx
205
275
  // that should not be passed to a wrapped component
206
276
  let stateToPass = _.omit(this.state, 'loading');
207
- stateToPass = _.omit(stateToPass, value => _.isNull(value));
208
-
209
- // Remove any null values so that React replaces them with default props
210
- const propsToPass = _.omit(this.props, value => _.isNull(value));
277
+ stateToPass = _.omit(stateToPass, _.isNull);
211
278
 
212
279
  // Spreading props and state is necessary in an HOC where the data cannot be predicted
213
280
  return (
214
281
  <WrappedComponent
282
+ markReadyForHydration={this.flushPendingSetStates}
215
283
  // eslint-disable-next-line react/jsx-props-no-spreading
216
284
  {...propsToPass}
217
285
  // eslint-disable-next-line react/jsx-props-no-spreading
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",