react-native-onyx 1.0.55 → 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.
package/lib/Onyx.js CHANGED
@@ -184,6 +184,46 @@ function isSafeEvictionKey(testKey) {
184
184
  return _.some(evictionAllowList, key => isKeyMatch(key, testKey));
185
185
  }
186
186
 
187
+ /**
188
+ * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
189
+ * If the requested key is a collection, it will return an object with all the collection members.
190
+ *
191
+ * @param {String} key
192
+ * @param {Object} mapping
193
+ * @returns {Mixed}
194
+ */
195
+ function tryGetCachedValue(key, mapping = {}) {
196
+ let val = cache.getValue(key);
197
+
198
+ if (isCollectionKey(key)) {
199
+ const allKeys = cache.getAllKeys();
200
+ const matchingKeys = _.filter(allKeys, k => k.startsWith(key));
201
+ const values = _.reduce(matchingKeys, (finalObject, matchedKey) => {
202
+ const cachedValue = cache.getValue(matchedKey);
203
+ if (cachedValue) {
204
+ // This is permissible because we're in the process of constructing the final object in a reduce function.
205
+ // eslint-disable-next-line no-param-reassign
206
+ finalObject[matchedKey] = cachedValue;
207
+ }
208
+ return finalObject;
209
+ }, {});
210
+ if (_.isEmpty(values)) {
211
+ return;
212
+ }
213
+ val = values;
214
+ }
215
+
216
+ if (mapping.selector) {
217
+ const state = mapping.withOnyxInstance ? mapping.withOnyxInstance.state : undefined;
218
+ if (isCollectionKey(key)) {
219
+ return reduceCollectionWithSelector(val, mapping.selector, state);
220
+ }
221
+ return getSubsetOfData(val, mapping.selector, state);
222
+ }
223
+
224
+ return val;
225
+ }
226
+
187
227
  /**
188
228
  * Remove a key from the recently accessed key list.
189
229
  *
@@ -1353,6 +1393,7 @@ const Onyx = {
1353
1393
  isSafeEvictionKey,
1354
1394
  METHOD,
1355
1395
  setMemoryOnlyKeys,
1396
+ tryGetCachedValue,
1356
1397
  };
1357
1398
 
1358
1399
  /**
package/lib/withOnyx.js CHANGED
@@ -37,13 +37,25 @@ export default function (mapOnyxToState) {
37
37
  // disconnected. It is a key value store with the format {[mapping.key]: connectionID}.
38
38
  this.activeConnectionIDs = {};
39
39
 
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
+ if (value !== undefined) {
45
+ // eslint-disable-next-line no-param-reassign
46
+ resultObj[propertyName] = value;
47
+ }
48
+
49
+ return resultObj;
50
+ }, {});
51
+
52
+ // If we have all the data we need, then we can render the component immediately
53
+ cachedState.loading = _.size(cachedState) < requiredKeysForInit.length;
54
+
40
55
  // Object holding the temporary initial state for the component while we load the various Onyx keys
41
- this.tempState = {};
56
+ this.tempState = cachedState;
42
57
 
43
- this.state = {
44
- // If there are no required keys for init then we can render the wrapped component immediately
45
- loading: requiredKeysForInit.length > 0,
46
- };
58
+ this.state = cachedState;
47
59
  }
48
60
 
49
61
  componentDidMount() {
@@ -60,7 +72,6 @@ export default function (mapOnyxToState) {
60
72
  _.each(mapOnyxToState, (mapping, propertyName) => {
61
73
  const previousKey = Str.result(mapping.key, prevProps);
62
74
  const newKey = Str.result(mapping.key, this.props);
63
-
64
75
  if (previousKey !== newKey) {
65
76
  Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey);
66
77
  delete this.activeConnectionIDs[previousKey];
@@ -88,6 +99,16 @@ export default function (mapOnyxToState) {
88
99
  * @param {*} val
89
100
  */
90
101
  setWithOnyxState(statePropertyName, val) {
102
+ // We might have loaded the values for the onyx keys/mappings already from the cache.
103
+ // In case we were able to load all the values upfront, the loading state will be false.
104
+ // However, Onyx.js will always call setWithOnyxState, as it doesn't know that this implementation
105
+ // already loaded the values from cache. Thus we have to check whether the value has changed
106
+ // before we set the state to prevent unnecessary renders.
107
+ const prevValue = this.state[statePropertyName];
108
+ if (!this.state.loading && prevValue === val) {
109
+ return;
110
+ }
111
+
91
112
  if (!this.state.loading) {
92
113
  this.setState({[statePropertyName]: val});
93
114
  return;
@@ -100,7 +121,14 @@ export default function (mapOnyxToState) {
100
121
  return;
101
122
  }
102
123
 
103
- this.setState({...this.tempState, loading: false});
124
+ const stateUpdate = {...this.tempState, loading: false};
125
+
126
+ // The state is set here manually, instead of using setState because setState is an async operation, meaning it might execute on the next tick,
127
+ // or at a later point in the microtask queue. That can lead to a race condition where
128
+ // 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.
129
+ this.state = stateUpdate;
130
+
131
+ this.setState(stateUpdate); // Trigger a render
104
132
  delete this.tempState;
105
133
  }
106
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.55",
3
+ "version": "1.0.56",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",