Skip to content

Commit 9a6fd30

Browse files
committed
Address comments
1 parent eac6774 commit 9a6fd30

3 files changed

Lines changed: 57 additions & 18 deletions

File tree

lib/OnyxCache.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -344,22 +344,15 @@ class OnyxCache {
344344
iterResult = iterator.next();
345345
}
346346

347-
const affectedCollections = new Set<OnyxKey>();
348-
349347
for (const key of keysToRemove) {
350348
delete this.storageMap[key];
351349

352-
// Track affected collections for snapshot rebuild
353350
const collectionKey = OnyxKeys.getCollectionKey(key);
354351
if (collectionKey) {
355-
affectedCollections.add(collectionKey);
352+
this.dirtyCollections.add(collectionKey);
356353
}
357354
this.recentKeys.delete(key);
358355
}
359-
360-
for (const collectionKey of affectedCollections) {
361-
this.dirtyCollections.add(collectionKey);
362-
}
363356
}
364357

365358
/** Set the recent keys list size */
@@ -478,10 +471,10 @@ class OnyxCache {
478471
* @param collectionKey - The collection key to rebuild
479472
*/
480473
private rebuildCollectionSnapshot(collectionKey: OnyxKey): void {
481-
const oldSnapshot = this.collectionSnapshots.get(collectionKey);
474+
const previousSnapshot = this.collectionSnapshots.get(collectionKey);
482475

483476
const members: NonUndefined<OnyxCollection<KeyValueMapping[OnyxKey]>> = {};
484-
let hasChanges = false;
477+
let hasMemberChanges = false;
485478
let newMemberCount = 0;
486479

487480
// Use the indexed forward lookup for O(collectionMembers) iteration.
@@ -495,29 +488,31 @@ class OnyxCache {
495488
continue;
496489
}
497490
const val = this.storageMap[key];
491+
// Skip null/undefined values — they represent deleted or unset keys
492+
// and should not be included in the frozen collection snapshot.
498493
if (val !== undefined && val !== null) {
499494
members[key] = val;
500495
newMemberCount++;
501496

502497
// Check if this member's reference changed from the old snapshot
503-
if (!hasChanges && (!oldSnapshot || oldSnapshot[key] !== val)) {
504-
hasChanges = true;
498+
if (!hasMemberChanges && (!previousSnapshot || previousSnapshot[key] !== val)) {
499+
hasMemberChanges = true;
505500
}
506501
}
507502
}
508503

509504
// Check if any members were removed (old snapshot had more keys)
510-
if (!hasChanges && oldSnapshot) {
511-
const oldMemberCount = Object.keys(oldSnapshot).length;
505+
if (!hasMemberChanges && previousSnapshot) {
506+
const oldMemberCount = Object.keys(previousSnapshot).length;
512507
if (oldMemberCount !== newMemberCount) {
513-
hasChanges = true;
508+
hasMemberChanges = true;
514509
}
515510
}
516511

517512
// If nothing actually changed, reuse the old snapshot reference.
518513
// This is critical: useSyncExternalStore uses === to detect changes,
519514
// so returning the same reference prevents unnecessary re-renders.
520-
if (!hasChanges && oldSnapshot) {
515+
if (!hasMemberChanges && previousSnapshot) {
521516
return;
522517
}
523518

@@ -538,7 +533,7 @@ class OnyxCache {
538533
}
539534

540535
const snapshot = this.collectionSnapshots.get(collectionKey);
541-
if (!snapshot || Object.keys(snapshot).length === 0) {
536+
if (utils.isEmptyObject(snapshot)) {
542537
// If we know we have storage keys loaded, return a stable empty reference
543538
// to avoid new {} allocations that break useSyncExternalStore === equality.
544539
if (this.storageKeys.size > 0) {

lib/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,19 @@ function mergeObject<TObject extends Record<string, unknown>>(
172172

173173
/** Checks whether the given object is an object and not null/undefined. */
174174
function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue {
175-
return typeof obj === 'object' && Object.keys(obj || {}).length === 0;
175+
if (typeof obj !== 'object') {
176+
return false;
177+
}
178+
179+
// Use for-in loop to avoid an unnecessary array allocation from Object.keys()
180+
// eslint-disable-next-line no-restricted-syntax
181+
for (const key in obj) {
182+
if (Object.hasOwn(obj, key)) {
183+
return false;
184+
}
185+
}
186+
187+
return true;
176188
}
177189

178190
/**

tests/unit/utilsTest.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,36 @@ describe('utils', () => {
389389
});
390390
});
391391
});
392+
393+
describe('isEmptyObject', () => {
394+
it('should return true for an empty object', () => {
395+
expect(utils.isEmptyObject({})).toBe(true);
396+
});
397+
398+
it('should return true for null', () => {
399+
expect(utils.isEmptyObject(null)).toBe(true);
400+
});
401+
402+
it('should return false for undefined', () => {
403+
expect(utils.isEmptyObject(undefined)).toBe(false);
404+
});
405+
406+
it('should return false for an object with properties', () => {
407+
expect(utils.isEmptyObject({a: 1})).toBe(false);
408+
});
409+
410+
it('should return false for non-object types', () => {
411+
expect(utils.isEmptyObject('hello')).toBe(false);
412+
expect(utils.isEmptyObject(42)).toBe(false);
413+
expect(utils.isEmptyObject(true)).toBe(false);
414+
});
415+
416+
it('should return true for an empty array', () => {
417+
expect(utils.isEmptyObject([])).toBe(true);
418+
});
419+
420+
it('should return false for a non-empty array', () => {
421+
expect(utils.isEmptyObject([1, 2])).toBe(false);
422+
});
423+
});
392424
});

0 commit comments

Comments
 (0)