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/API.md +27 -17
- package/README.md +68 -1
- package/dist/web.development.js +214 -144
- package/dist/web.development.js.map +1 -1
- package/dist/web.min.js +1 -1
- package/dist/web.min.js.map +1 -1
- package/lib/Onyx.js +27 -15
- package/lib/OnyxCache.js +2 -2
- package/lib/storage/__mocks__/index.js +2 -2
- package/lib/storage/providers/IDBKeyVal.js +3 -4
- package/lib/utils.js +76 -2
- package/lib/withOnyx.d.ts +1 -0
- package/lib/withOnyx.js +115 -47
- package/package.json +1 -1
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
* still in a loading state. The temporary initial state is saved to the
|
|
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
|
-
|
|
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
|
-
//
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
172
|
+
const finalState = _.reduce(stateUpdate, (result, value, key) => {
|
|
173
|
+
if (key === 'loading') {
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
140
176
|
|
|
141
|
-
|
|
142
|
-
});
|
|
177
|
+
const initialValue = mapOnyxToState[key].initialValue;
|
|
143
178
|
|
|
144
|
-
|
|
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,
|
|
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
|