Skip to content

Commit 1cc7acc

Browse files
authored
Merge pull request Expensify#799 from callstack-internal/feature/onyx-store-pr-1
Remove waitForCollectionCallback from Onyx
2 parents 608da63 + 721d95b commit 1cc7acc

13 files changed

Lines changed: 157 additions & 366 deletions

API.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ This method will be deprecated soon. Please use `Onyx.connectWithoutView()` inst
8080
| connectOptions | The options object that will define the behavior of the connection. |
8181
| connectOptions.key | The Onyx key to subscribe to. |
8282
| connectOptions.callback | A function that will be called when the Onyx data we are subscribed changes. |
83-
| connectOptions.waitForCollectionCallback | If set to `true`, it will return the entire collection to the callback as a single object. |
8483
| connectOptions.selector | This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.** Using this setting on `useOnyx()` 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). |
8584

8685
**Example**
@@ -103,7 +102,6 @@ Connects to an Onyx key given the options passed and listens to its changes.
103102
| connectOptions | The options object that will define the behavior of the connection. |
104103
| connectOptions.key | The Onyx key to subscribe to. |
105104
| connectOptions.callback | A function that will be called when the Onyx data we are subscribed changes. |
106-
| connectOptions.waitForCollectionCallback | If set to `true`, it will return the entire collection to the callback as a single object. |
107105
| connectOptions.selector | This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.** Using this setting on `useOnyx()` 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). |
108106

109107
**Example**

README.md

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -257,20 +257,10 @@ export default MyComponent;
257257
This will add a prop to the component called `allReports` which is an object of collection member key/values. Changes to the individual member keys will modify the entire object and new props will be passed with each individual key update. The prop doesn't update on the initial rendering of the component until the entire collection has been read out of Onyx.
258258

259259
```js
260-
Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (memberValue, memberKey) => {...});
260+
Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (allReports, collectionKey, sourceValue) => {...});
261261
```
262262

263-
This will fire the callback once per member key depending on how many collection member keys are currently stored. Changes to those keys after the initial callbacks fire will occur when each individual key is updated.
264-
265-
```js
266-
Onyx.connect({
267-
key: ONYXKEYS.COLLECTION.REPORT,
268-
waitForCollectionCallback: true,
269-
callback: (allReports, collectionKey, sourceValue) => {...},
270-
});
271-
```
272-
273-
This final option forces `Onyx.connect()` to behave more like `useOnyx()` and only update the callback once with the entire collection initially and later with an updated version of the collection when individual keys update. The `sourceValue` parameter contains only the specific keys and values that triggered the current update, which can be useful for optimizing your code to only process what changed. This parameter is not available when `waitForCollectionCallback` is false or the key is not a collection key.
263+
This will fire the callback once with the entire collection initially and later with an updated version of the collection when individual keys update. The `sourceValue` parameter contains only the specific keys and values that triggered the current update, which can be useful for optimizing your code to only process what changed.
274264

275265
### Performance Considerations When Using Collections
276266

lib/Onyx.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ function init({
9494
* @param connectOptions The options object that will define the behavior of the connection.
9595
* @param connectOptions.key The Onyx key to subscribe to.
9696
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
97-
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
9897
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
9998
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
10099
* when the subset of data changes. Otherwise, any change of data on any property would normally
@@ -119,7 +118,6 @@ function connect<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKey>): Co
119118
* @param connectOptions The options object that will define the behavior of the connection.
120119
* @param connectOptions.key The Onyx key to subscribe to.
121120
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
122-
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
123121
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
124122
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
125123
* when the subset of data changes. Otherwise, any change of data on any property would normally

lib/OnyxConnectionManager.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {ConnectOptions} from './Onyx';
44
import OnyxUtils from './OnyxUtils';
55
import OnyxKeys from './OnyxKeys';
66
import * as Str from './Str';
7-
import type {CollectionConnectCallback, DefaultConnectCallback, DefaultConnectOptions, OnyxKey, OnyxValue} from './types';
7+
import type {CollectionConnectCallback, DefaultConnectCallback, OnyxKey, OnyxValue} from './types';
88
import onyxSnapshotCache from './OnyxSnapshotCache';
99

1010
type ConnectCallback = DefaultConnectCallback<OnyxKey> | CollectionConnectCallback<OnyxKey>;
@@ -48,11 +48,6 @@ type ConnectionMetadata = {
4848
* The value that triggered the last update
4949
*/
5050
sourceValue?: OnyxValue<OnyxKey>;
51-
52-
/**
53-
* Whether the subscriber is waiting for the collection callback to be fired.
54-
*/
55-
waitForCollectionCallback?: boolean;
5651
};
5752

5853
/**
@@ -115,21 +110,20 @@ class OnyxConnectionManager {
115110
* according to their purpose and effect they produce in the Onyx connection.
116111
*/
117112
private generateConnectionID<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKey>): string {
118-
const {key, reuseConnection, waitForCollectionCallback} = connectOptions;
113+
const {key, reuseConnection} = connectOptions;
119114

120115
// The current session ID is appended to the connection ID so we can have different connections
121116
// after an `Onyx.clear()` operation.
122117
let suffix = `,sessionID=${this.sessionID}`;
123118

124-
// We will generate a unique ID in any of the following situations:
125-
// - `reuseConnection` is `false`. That means the subscriber explicitly wants the connection to not be reused.
126-
// - `key` is a collection key AND `waitForCollectionCallback` is `undefined/false`. This combination needs a new connection at every subscription
127-
// in order to send all the collection entries, so the connection can't be reused.
128-
if (reuseConnection === false || (OnyxKeys.isCollectionKey(key) && (waitForCollectionCallback === undefined || waitForCollectionCallback === false))) {
119+
// We will generate a unique ID when `reuseConnection` is `false`, which means the subscriber
120+
// explicitly wants the connection to not be reused. Collection-root subscriptions are now always
121+
// snapshot mode, so they can be reused like any other connection.
122+
if (reuseConnection === false) {
129123
suffix += `,uniqueID=${Str.guid()}`;
130124
}
131125

132-
return `onyxKey=${key},waitForCollectionCallback=${waitForCollectionCallback ?? false}${suffix}`;
126+
return `onyxKey=${key}${suffix}`;
133127
}
134128

135129
/**
@@ -142,10 +136,18 @@ class OnyxConnectionManager {
142136
}
143137

144138
for (const callback of connection.callbacks.values()) {
145-
if (connection.waitForCollectionCallback) {
146-
(callback as CollectionConnectCallback<OnyxKey>)(connection.cachedCallbackValue as Record<string, unknown>, connection.cachedCallbackKey as OnyxKey, connection.sourceValue);
147-
} else {
148-
(callback as DefaultConnectCallback<OnyxKey>)(connection.cachedCallbackValue, connection.cachedCallbackKey as OnyxKey);
139+
try {
140+
if (OnyxKeys.isCollectionKey(connection.onyxKey)) {
141+
(callback as CollectionConnectCallback<OnyxKey>)(
142+
connection.cachedCallbackValue as Record<string, unknown>,
143+
connection.cachedCallbackKey as OnyxKey,
144+
connection.sourceValue,
145+
);
146+
} else {
147+
(callback as DefaultConnectCallback<OnyxKey>)(connection.cachedCallbackValue, connection.cachedCallbackKey as OnyxKey);
148+
}
149+
} catch (error) {
150+
Logger.logAlert(`[ConnectionManager] Subscriber callback threw an error for key '${connection.onyxKey}': ${error}`);
149151
}
150152
}
151153
}
@@ -180,15 +182,14 @@ class OnyxConnectionManager {
180182

181183
subscriptionID = OnyxUtils.subscribeToKey({
182184
...connectOptions,
183-
callback: callback as DefaultConnectCallback<TKey>,
184-
});
185+
callback,
186+
} as ConnectOptions<TKey>);
185187

186188
connectionMetadata = {
187189
subscriptionID,
188190
onyxKey: connectOptions.key,
189191
isConnectionMade: false,
190192
callbacks: new Map(),
191-
waitForCollectionCallback: connectOptions.waitForCollectionCallback,
192193
};
193194

194195
this.connectionsMap.set(connectionID, connectionMetadata);
@@ -204,7 +205,7 @@ class OnyxConnectionManager {
204205
// Defer the callback execution to the next tick of the event loop.
205206
// This ensures that the current execution flow completes and the result connection object is available when the callback fires.
206207
Promise.resolve().then(() => {
207-
(connectOptions as DefaultConnectOptions<OnyxKey>).callback?.(connectionMetadata.cachedCallbackValue, connectionMetadata.cachedCallbackKey as OnyxKey);
208+
(connectOptions.callback as DefaultConnectCallback<OnyxKey> | undefined)?.(connectionMetadata.cachedCallbackValue, connectionMetadata.cachedCallbackKey as OnyxKey);
208209
});
209210
}
210211

lib/OnyxUtils.ts

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import StorageCircuitBreaker from './StorageCircuitBreaker';
1010
import Storage from './storage';
1111
import {StorageErrorClass} from './storage/errors';
1212
import type {
13+
CollectionConnectCallback,
1314
CollectionKeyBase,
1415
ConnectOptions,
1516
DeepRecord,
1617
DefaultConnectCallback,
17-
DefaultConnectOptions,
1818
KeyValueMapping,
1919
CallbackToStateMapping,
2020
MultiMergeReplaceNullPatches,
@@ -580,31 +580,8 @@ function keysChanged<TKey extends CollectionKeyBase>(
580580
}
581581

582582
try {
583-
lastConnectionCallbackData.set(subscriber.subscriptionID, {
584-
value: cachedCollection,
585-
matchedKey: subscriber.key,
586-
});
587-
588-
if (subscriber.waitForCollectionCallback) {
589-
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
590-
continue;
591-
}
592-
593-
// Not using waitForCollectionCallback — notify per changed key.
594-
// Re-check the subscription on each iteration because the callback may
595-
// synchronously disconnect itself (removing it from callbackToStateMapping),
596-
// in which case we must stop firing further callbacks for this subscriber.
597-
for (const dataKey of changedMemberKeys) {
598-
const currentSubscriber = callbackToStateMapping[subID];
599-
if (!currentSubscriber || typeof currentSubscriber.callback !== 'function') {
600-
break;
601-
}
602-
if (cachedCollection[dataKey] === previousCollection[dataKey]) {
603-
continue;
604-
}
605-
const currentSubscriberCallback = currentSubscriber.callback as DefaultConnectCallback<TKey>;
606-
currentSubscriberCallback(cachedCollection[dataKey], dataKey as TKey);
607-
}
583+
lastConnectionCallbackData.set(subscriber.subscriptionID, {value: cachedCollection, matchedKey: subscriber.key});
584+
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
608585
} catch (error) {
609586
Logger.logAlert(`[OnyxUtils.keysChanged] Subscriber callback threw an error for key '${collectionKey}': ${error}`);
610587
}
@@ -685,9 +662,9 @@ function keyChanged<TKey extends OnyxKey>(
685662
continue;
686663
}
687664

688-
if (OnyxKeys.isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
689-
// Skip individual key changes for collection callbacks during collection updates
690-
// to prevent duplicate callbacks - the collection update will handle this properly
665+
if (OnyxKeys.isCollectionKey(subscriber.key)) {
666+
// Skip individual key changes during collection updates to prevent duplicate
667+
// callbacks - the collection update will handle this properly.
691668
if (isProcessingCollectionUpdate) {
692669
continue;
693670
}
@@ -698,13 +675,8 @@ function keyChanged<TKey extends OnyxKey>(
698675
cachedCollection = getCachedCollection(subscriber.key);
699676
cachedCollections[subscriber.key] = cachedCollection;
700677
}
701-
lastConnectionCallbackData.set(subscriber.subscriptionID, {
702-
value: cachedCollection,
703-
matchedKey: subscriber.key,
704-
});
705-
subscriber.callback(cachedCollection, subscriber.key, {
706-
[key]: value,
707-
});
678+
lastConnectionCallbackData.set(subscriber.subscriptionID, {value: cachedCollection, matchedKey: subscriber.key});
679+
(subscriber.callback as CollectionConnectCallback<OnyxKey>)(cachedCollection, subscriber.key, {[key]: value});
708680
continue;
709681
}
710682

@@ -738,10 +710,10 @@ function sendDataToConnection<TKey extends OnyxKey>(mapping: CallbackToStateMapp
738710
}
739711

740712
// Always read the latest value from cache to avoid stale or duplicate data.
741-
// For collection subscribers with waitForCollectionCallback, read the full collection.
713+
// For collection-root subscribers, read the full collection.
742714
// For individual key subscribers, read just that key's value.
743715
let value: OnyxValue<TKey> | undefined;
744-
if (OnyxKeys.isCollectionKey(mapping.key) && mapping.waitForCollectionCallback) {
716+
if (OnyxKeys.isCollectionKey(mapping.key)) {
745717
const collection = getCachedCollection(mapping.key);
746718
value = Object.keys(collection).length > 0 ? (collection as OnyxValue<TKey>) : undefined;
747719
} else {
@@ -759,7 +731,7 @@ function sendDataToConnection<TKey extends OnyxKey>(mapping: CallbackToStateMapp
759731
return;
760732
}
761733

762-
(mapping as DefaultConnectOptions<TKey>).callback?.(value, matchedKey as TKey);
734+
(mapping.callback as DefaultConnectCallback<TKey> | undefined)?.(value, matchedKey as TKey);
763735
}
764736

765737
/**
@@ -1180,30 +1152,19 @@ function subscribeToKey<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKe
11801152
cache.addNullishStorageKey(mapping.key);
11811153
}
11821154

1183-
const matchedKey = OnyxKeys.isCollectionKey(mapping.key) && mapping.waitForCollectionCallback ? mapping.key : undefined;
1155+
const matchedKey = OnyxKeys.isCollectionKey(mapping.key) ? mapping.key : undefined;
11841156

11851157
// Here we cannot use batching because the nullish value is expected to be set immediately for default props
11861158
// or they will be undefined.
11871159
sendDataToConnection(mapping, matchedKey);
11881160
return;
11891161
}
11901162

1191-
// When using a callback subscriber we will either trigger the provided callback for each key we find or combine all values
1192-
// into an object and just make a single call. The latter behavior is enabled by providing a waitForCollectionCallback key
1193-
// combined with a subscription to a collection key.
1163+
// When using a callback subscriber, a subscription to a collection key combines all matching
1164+
// member values into a single object and makes one call with the whole collection object.
11941165
if (typeof mapping.callback === 'function') {
11951166
if (OnyxKeys.isCollectionKey(mapping.key)) {
1196-
if (mapping.waitForCollectionCallback) {
1197-
getCollectionDataAndSendAsObject(matchingKeys, mapping);
1198-
return;
1199-
}
1200-
1201-
// We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
1202-
multiGet(matchingKeys).then(() => {
1203-
for (const key of matchingKeys) {
1204-
sendDataToConnection(mapping, key as TKey);
1205-
}
1206-
});
1167+
getCollectionDataAndSendAsObject(matchingKeys, mapping);
12071168
return;
12081169
}
12091170

@@ -1463,7 +1424,7 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom
14631424
// and subscriber state, matching the original per-key notification semantics.
14641425
cache.set(key, value);
14651426
// Skip subscriber notification on retry — already notified on attempt 0.
1466-
// waitForCollectionCallback subscribers re-fire on every keyChanged by contract.
1427+
// Collection-root subscribers re-fire on every keyChanged by contract.
14671428
if (!retryAttempt) {
14681429
keyChanged(key, value);
14691430
}
@@ -1554,7 +1515,7 @@ function setCollectionWithRetry<TKey extends CollectionKeyBase>({collectionKey,
15541515
for (const [key, value] of keyValuePairs) cache.set(key, value);
15551516

15561517
// Skip subscriber notification on retry — already notified on attempt 0.
1557-
// waitForCollectionCallback subscribers re-fire on every keysChanged by contract.
1518+
// Collection-root subscribers re-fire on every keysChanged by contract.
15581519
if (!retryAttempt) {
15591520
keysChanged(collectionKey, mutableCollection, previousCollection);
15601521
}
@@ -1704,7 +1665,7 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(
17041665
const previousCollection = getCachedCollection(collectionKey, existingKeys);
17051666
cache.merge(finalMergedCollection);
17061667
// Skip subscriber notification on retry — already notified on attempt 0.
1707-
// waitForCollectionCallback subscribers re-fire on every keysChanged by contract.
1668+
// Collection-root subscribers re-fire on every keysChanged by contract.
17081669
if (!retryAttempt) {
17091670
keysChanged(collectionKey, finalMergedCollection, previousCollection);
17101671
}
@@ -1797,7 +1758,7 @@ function partialSetCollection<TKey extends CollectionKeyBase>({collectionKey, co
17971758
for (const [key, value] of keyValuePairs) cache.set(key, value);
17981759

17991760
// Skip subscriber notification on retry — already notified on attempt 0.
1800-
// waitForCollectionCallback subscribers re-fire on every keysChanged by contract.
1761+
// Collection-root subscribers re-fire on every keysChanged by contract.
18011762
if (!retryAttempt) {
18021763
keysChanged(collectionKey, mutableCollection, previousCollection);
18031764
}

0 commit comments

Comments
 (0)