Skip to content

Commit 340b574

Browse files
authored
Merge pull request #622 from callstack-internal/feat/expose-callback-trigger
feat: expose callback trigger value for collections
2 parents 02916f5 + 61f8e89 commit 340b574

7 files changed

+129
-24
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,11 @@ This will fire the callback once per member key depending on how many collection
331331
Onyx.connect({
332332
key: ONYXKEYS.COLLECTION.REPORT,
333333
waitForCollectionCallback: true,
334-
callback: (allReports) => {...},
334+
callback: (allReports, collectionKey, sourceValue) => {...},
335335
});
336336
```
337337

338-
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.
338+
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.
339339

340340
### Performance Considerations When Using Collections
341341

lib/OnyxConnectionManager.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import * as Logger from './Logger';
33
import type {ConnectOptions} from './Onyx';
44
import OnyxUtils from './OnyxUtils';
55
import * as Str from './Str';
6-
import type {DefaultConnectCallback, DefaultConnectOptions, OnyxKey, OnyxValue} from './types';
6+
import type {CollectionConnectCallback, DefaultConnectCallback, DefaultConnectOptions, OnyxKey, OnyxValue} from './types';
77
import utils from './utils';
88

9-
type ConnectCallback = DefaultConnectCallback<OnyxKey>;
9+
type ConnectCallback = DefaultConnectCallback<OnyxKey> | CollectionConnectCallback<OnyxKey>;
1010

1111
/**
1212
* Represents the connection's metadata that contains the necessary properties
@@ -42,6 +42,16 @@ type ConnectionMetadata = {
4242
* The last callback key returned by `OnyxUtils.subscribeToKey()`'s callback.
4343
*/
4444
cachedCallbackKey?: OnyxKey;
45+
46+
/**
47+
* The value that triggered the last update
48+
*/
49+
sourceValue?: OnyxValue<OnyxKey>;
50+
51+
/**
52+
* Whether the subscriber is waiting for the collection callback to be fired.
53+
*/
54+
waitForCollectionCallback?: boolean;
4555
};
4656

4757
/**
@@ -135,7 +145,11 @@ class OnyxConnectionManager {
135145
const connection = this.connectionsMap.get(connectionID);
136146

137147
connection?.callbacks.forEach((callback) => {
138-
callback(connection.cachedCallbackValue, connection.cachedCallbackKey as OnyxKey);
148+
if (connection.waitForCollectionCallback) {
149+
(callback as CollectionConnectCallback<OnyxKey>)(connection.cachedCallbackValue as Record<string, unknown>, connection.cachedCallbackKey as OnyxKey, connection.sourceValue);
150+
} else {
151+
(callback as DefaultConnectCallback<OnyxKey>)(connection.cachedCallbackValue, connection.cachedCallbackKey as OnyxKey);
152+
}
139153
});
140154
}
141155

@@ -159,30 +173,31 @@ class OnyxConnectionManager {
159173
// If the subscriber is a `withOnyx` HOC we don't define `callback` as the HOC will use
160174
// its own logic to handle the data.
161175
if (!utils.hasWithOnyxInstance(connectOptions)) {
162-
callback = (value, key) => {
176+
callback = (value, key, sourceValue) => {
163177
const createdConnection = this.connectionsMap.get(connectionID);
164178
if (createdConnection) {
165179
// We signal that the first connection was made and now any new subscribers
166180
// can fire their callbacks immediately with the cached value when connecting.
167181
createdConnection.isConnectionMade = true;
168182
createdConnection.cachedCallbackValue = value;
169183
createdConnection.cachedCallbackKey = key;
170-
184+
createdConnection.sourceValue = sourceValue;
171185
this.fireCallbacks(connectionID);
172186
}
173187
};
174188
}
175189

176190
subscriptionID = OnyxUtils.subscribeToKey({
177-
...(connectOptions as DefaultConnectOptions<OnyxKey>),
178-
callback,
191+
...connectOptions,
192+
callback: callback as DefaultConnectCallback<TKey>,
179193
});
180194

181195
connectionMetadata = {
182196
subscriptionID,
183197
onyxKey: connectOptions.key,
184198
isConnectionMade: false,
185199
callbacks: new Map(),
200+
waitForCollectionCallback: connectOptions.waitForCollectionCallback,
186201
};
187202

188203
this.connectionsMap.set(connectionID, connectionMetadata);

lib/OnyxUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ function keysChanged<TKey extends CollectionKeyBase>(
695695
// send the whole cached collection.
696696
if (isSubscribedToCollectionKey) {
697697
if (subscriber.waitForCollectionCallback) {
698-
subscriber.callback(cachedCollection, subscriber.key);
698+
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
699699
continue;
700700
}
701701

@@ -905,7 +905,7 @@ function keyChanged<TKey extends OnyxKey>(
905905
}
906906

907907
cachedCollection[key] = value;
908-
subscriber.callback(cachedCollection, subscriber.key);
908+
subscriber.callback(cachedCollection, subscriber.key, {[key]: value});
909909
continue;
910910
}
911911

lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ type BaseConnectOptions = {
278278
type DefaultConnectCallback<TKey extends OnyxKey> = (value: OnyxEntry<KeyValueMapping[TKey]>, key: TKey) => void;
279279

280280
/** Represents the callback function used in `Onyx.connect()` method with a collection key. */
281-
type CollectionConnectCallback<TKey extends OnyxKey> = (value: NonUndefined<OnyxCollection<KeyValueMapping[TKey]>>, key: TKey) => void;
281+
type CollectionConnectCallback<TKey extends OnyxKey> = (value: NonUndefined<OnyxCollection<KeyValueMapping[TKey]>>, key: TKey, sourceValue?: OnyxValue<TKey>) => void;
282282

283283
/** Represents the options used in `Onyx.connect()` method with a regular key. */
284284
// NOTE: Any changes to this type like adding or removing options must be accounted in OnyxConnectionManager's `generateConnectionID()` method!

tests/unit/OnyxConnectionManagerTest.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ describe('OnyxConnectionManager', () => {
164164
expect(callback1).toHaveBeenNthCalledWith(2, obj2, `${ONYXKEYS.COLLECTION.TEST_KEY}entry2`);
165165

166166
expect(callback2).toHaveBeenCalledTimes(1);
167-
expect(callback2).toHaveBeenCalledWith(collection, undefined);
167+
expect(callback2).toHaveBeenCalledWith(collection, undefined, undefined);
168168

169169
connectionManager.disconnect(connection1);
170170
connectionManager.disconnect(connection2);
@@ -544,4 +544,71 @@ describe('OnyxConnectionManager', () => {
544544
}).not.toThrow();
545545
});
546546
});
547+
548+
describe('sourceValue parameter', () => {
549+
it('should pass the sourceValue parameter to collection callbacks when waitForCollectionCallback is true', async () => {
550+
const obj1 = {id: 'entry1_id', name: 'entry1_name'};
551+
const obj2 = {id: 'entry2_id', name: 'entry2_name'};
552+
553+
const callback = jest.fn();
554+
const connection = connectionManager.connect({
555+
key: ONYXKEYS.COLLECTION.TEST_KEY,
556+
callback,
557+
waitForCollectionCallback: true,
558+
});
559+
560+
await act(async () => waitForPromisesToResolve());
561+
562+
// Initial callback with undefined values
563+
expect(callback).toHaveBeenCalledTimes(1);
564+
expect(callback).toHaveBeenCalledWith(undefined, undefined, undefined);
565+
566+
// Reset mock to test the next update
567+
callback.mockReset();
568+
569+
// Update with first object
570+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`, obj1);
571+
572+
expect(callback).toHaveBeenCalledTimes(1);
573+
expect(callback).toHaveBeenCalledWith({[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: obj1}, ONYXKEYS.COLLECTION.TEST_KEY, {[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: obj1});
574+
575+
// Reset mock to test the next update
576+
callback.mockReset();
577+
578+
// Update with second object
579+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`, obj2);
580+
581+
expect(callback).toHaveBeenCalledTimes(1);
582+
expect(callback).toHaveBeenCalledWith(
583+
{
584+
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: obj1,
585+
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: obj2,
586+
},
587+
ONYXKEYS.COLLECTION.TEST_KEY,
588+
{[`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: obj2},
589+
);
590+
591+
connectionManager.disconnect(connection);
592+
});
593+
594+
it('should not pass sourceValue to regular callbacks when waitForCollectionCallback is false', async () => {
595+
const obj1 = {id: 'entry1_id', name: 'entry1_name'};
596+
597+
const callback = jest.fn();
598+
const connection = connectionManager.connect({
599+
key: ONYXKEYS.COLLECTION.TEST_KEY,
600+
callback,
601+
waitForCollectionCallback: false,
602+
});
603+
604+
await act(async () => waitForPromisesToResolve());
605+
606+
// Update with object
607+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`, obj1);
608+
609+
expect(callback).toHaveBeenCalledWith(obj1, `${ONYXKEYS.COLLECTION.TEST_KEY}entry1`);
610+
611+
connectionManager.disconnect(connection);
612+
});
613+
});
547614
});

tests/unit/onyxClearWebStorageTest.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe('Set data while storage is clearing', () => {
161161
expect(collectionCallback).toHaveBeenCalledTimes(3);
162162

163163
// And it should be called with the expected parameters each time
164-
expect(collectionCallback).toHaveBeenNthCalledWith(1, undefined, undefined);
164+
expect(collectionCallback).toHaveBeenNthCalledWith(1, undefined, undefined, undefined);
165165
expect(collectionCallback).toHaveBeenNthCalledWith(
166166
2,
167167
{
@@ -171,8 +171,19 @@ describe('Set data while storage is clearing', () => {
171171
test_4: 4,
172172
},
173173
ONYX_KEYS.COLLECTION.TEST,
174+
{
175+
test_1: 1,
176+
test_2: 2,
177+
test_3: 3,
178+
test_4: 4,
179+
},
174180
);
175-
expect(collectionCallback).toHaveBeenLastCalledWith({}, ONYX_KEYS.COLLECTION.TEST);
181+
expect(collectionCallback).toHaveBeenLastCalledWith({}, ONYX_KEYS.COLLECTION.TEST, {
182+
test_1: undefined,
183+
test_2: undefined,
184+
test_3: undefined,
185+
test_4: undefined,
186+
});
176187
})
177188
);
178189
});

tests/unit/onyxTest.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ describe('Onyx', () => {
989989
.then(() => {
990990
// Then we expect the callback to be called only once and the initial stored value to be initialCollectionData
991991
expect(mockCallback).toHaveBeenCalledTimes(1);
992-
expect(mockCallback).toHaveBeenCalledWith(initialCollectionData, undefined);
992+
expect(mockCallback).toHaveBeenCalledWith(initialCollectionData, undefined, undefined);
993993
});
994994
});
995995

@@ -1015,10 +1015,10 @@ describe('Onyx', () => {
10151015
expect(mockCallback).toHaveBeenCalledTimes(2);
10161016

10171017
// AND the value for the first call should be null since the collection was not initialized at that point
1018-
expect(mockCallback).toHaveBeenNthCalledWith(1, undefined, undefined);
1018+
expect(mockCallback).toHaveBeenNthCalledWith(1, undefined, undefined, undefined);
10191019

10201020
// AND the value for the second call should be collectionUpdate since the collection was updated
1021-
expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY);
1021+
expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY, collectionUpdate);
10221022
})
10231023
);
10241024
});
@@ -1073,7 +1073,10 @@ describe('Onyx', () => {
10731073
expect(mockCallback).toHaveBeenCalledTimes(2);
10741074

10751075
// AND the value for the second call should be collectionUpdate
1076-
expect(mockCallback).toHaveBeenLastCalledWith(collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY);
1076+
expect(mockCallback).toHaveBeenNthCalledWith(1, undefined, undefined, undefined);
1077+
expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY, {
1078+
[`${ONYX_KEYS.COLLECTION.TEST_POLICY}1`]: collectionUpdate.testPolicy_1,
1079+
});
10771080
})
10781081
);
10791082
});
@@ -1108,7 +1111,7 @@ describe('Onyx', () => {
11081111
expect(mockCallback).toHaveBeenCalledTimes(2);
11091112

11101113
// And the value for the second call should be collectionUpdate
1111-
expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY);
1114+
expect(mockCallback).toHaveBeenNthCalledWith(2, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY, {testPolicy_1: collectionUpdate.testPolicy_1});
11121115
})
11131116

11141117
// When merge is called again with the same collection not modified
@@ -1149,7 +1152,7 @@ describe('Onyx', () => {
11491152
expect(mockCallback).toHaveBeenCalledTimes(1);
11501153

11511154
// And the value for the second call should be collectionUpdate
1152-
expect(mockCallback).toHaveBeenNthCalledWith(1, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY);
1155+
expect(mockCallback).toHaveBeenNthCalledWith(1, collectionUpdate, ONYX_KEYS.COLLECTION.TEST_POLICY, {testPolicy_1: collectionUpdate.testPolicy_1});
11531156
})
11541157

11551158
// When merge is called again with the same collection not modified
@@ -1186,8 +1189,8 @@ describe('Onyx', () => {
11861189
{onyxMethod: Onyx.METHOD.MERGE_COLLECTION, key: ONYX_KEYS.COLLECTION.TEST_UPDATE, value: {[itemKey]: {a: 'a'}}},
11871190
]).then(() => {
11881191
expect(collectionCallback).toHaveBeenCalledTimes(2);
1189-
expect(collectionCallback).toHaveBeenNthCalledWith(1, undefined, undefined);
1190-
expect(collectionCallback).toHaveBeenNthCalledWith(2, {[itemKey]: {a: 'a'}}, ONYX_KEYS.COLLECTION.TEST_UPDATE);
1192+
expect(collectionCallback).toHaveBeenNthCalledWith(1, undefined, undefined, undefined);
1193+
expect(collectionCallback).toHaveBeenNthCalledWith(2, {[itemKey]: {a: 'a'}}, ONYX_KEYS.COLLECTION.TEST_UPDATE, {[itemKey]: {a: 'a'}});
11911194

11921195
expect(testCallback).toHaveBeenCalledTimes(2);
11931196
expect(testCallback).toHaveBeenNthCalledWith(1, undefined, undefined);
@@ -1426,7 +1429,9 @@ describe('Onyx', () => {
14261429
})
14271430
.then(() => {
14281431
expect(collectionCallback).toHaveBeenCalledTimes(3);
1429-
expect(collectionCallback).toHaveBeenCalledWith(collectionDiff, ONYX_KEYS.COLLECTION.ANIMALS);
1432+
expect(collectionCallback).toHaveBeenNthCalledWith(1, {[cat]: initialValue}, ONYX_KEYS.COLLECTION.ANIMALS, {[cat]: initialValue});
1433+
expect(collectionCallback).toHaveBeenNthCalledWith(2, {[cat]: initialValue}, undefined, undefined);
1434+
expect(collectionCallback).toHaveBeenNthCalledWith(3, collectionDiff, ONYX_KEYS.COLLECTION.ANIMALS, {[cat]: initialValue, [dog]: {name: 'Rex'}});
14301435

14311436
// Cat hasn't changed from its original value, expect only the initial connect callback
14321437
expect(catCallback).toHaveBeenCalledTimes(1);
@@ -1558,6 +1563,10 @@ describe('Onyx', () => {
15581563
},
15591564
},
15601565
ONYX_KEYS.COLLECTION.ROUTES,
1566+
{
1567+
[holidayRoute]: {waypoints: {0: 'Bed', 1: 'Home', 2: 'Beach', 3: 'Restaurant', 4: 'Home'}},
1568+
[routineRoute]: {waypoints: {0: 'Bed', 1: 'Home', 2: 'Work', 3: 'Gym'}},
1569+
},
15611570
);
15621571

15631572
connections.map((id) => Onyx.disconnect(id));
@@ -1628,6 +1637,7 @@ describe('Onyx', () => {
16281637
[cat]: {age: 3, sound: 'meow'},
16291638
},
16301639
ONYX_KEYS.COLLECTION.ANIMALS,
1640+
{[cat]: {age: 3, sound: 'meow'}},
16311641
);
16321642
expect(animalsCollectionCallback).toHaveBeenNthCalledWith(
16331643
2,
@@ -1636,6 +1646,7 @@ describe('Onyx', () => {
16361646
[dog]: {size: 'M', sound: 'woof'},
16371647
},
16381648
ONYX_KEYS.COLLECTION.ANIMALS,
1649+
{[dog]: {size: 'M', sound: 'woof'}},
16391650
);
16401651

16411652
expect(catCallback).toHaveBeenNthCalledWith(1, {age: 3, sound: 'meow'}, cat);
@@ -1647,6 +1658,7 @@ describe('Onyx', () => {
16471658
[lisa]: {age: 21, car: 'SUV'},
16481659
},
16491660
ONYX_KEYS.COLLECTION.PEOPLE,
1661+
{[bob]: {age: 25, car: 'sedan'}, [lisa]: {age: 21, car: 'SUV'}},
16501662
);
16511663

16521664
connections.map((id) => Onyx.disconnect(id));

0 commit comments

Comments
 (0)