Add stableCollectionReference to avoid rerenders #718
+400
−2
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Details
Background
When a child component using useOnyx mounts with the same key as its parent, the parent receives a new reference on the next render. This can cause an additional, unnecessary re-render even though the data is unchanged (isDeepEqual: true, isSameRef: false). I tested a similar scenario using an object (non-collection) key, and this issue did not occur (see the simple test app:
).
Root Cause
getCollectionData() in OnyxCache.ts returns { ...cachedCollection }, which creates a new shallow copy on each call. This ensures React detects changes when items are added or removed, but may also replace reference in case if we add new subscribers with the same key. This can cause unnecessary rerenders in other parts of the app, triggered by unrelated events such as navigation.
Solution
Added memoization to getCollectionData to avoid creating new object references when the collection hasn't changed. The method tracks dirty collections via a dirtyCollections Set and caches the last returned reference in stableCollectionReference. If the collection isn't dirty and a cached reference exists, it returns that reference; otherwise it creates a new reference, stores it, and marks the collection as clean. This ensures multiple calls for an unchanged collection return the same reference, preserving React's reference equality checks and reducing unnecessary re-renders.
Results
Before fix (test app, main):
https://github.com/user-attachments/assets/daee6b0a-35e0-470b-87ef-b66f62b93e12
After fix (test app, PR)
https://github.com/user-attachments/assets/f27014ce-6827-401b-a2c3-036eac4cd0d9
Expensify app, open report scenario (4x slowdown)

Related Issues
Expensify/App#77173
Automated Tests
I added both perf and unit tests. I checked locally with baseline created on "main" and these are diff results. Instant access for collections stored as stable, and a bit slower when we return new reference. Main benefit is visible in real app, when we can reduce some rerenders with it.
Manual Tests
I recommend performing at least the following steps:
Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
android-native.mov
Android: mWeb Chrome
android-web.mov
iOS: Native
ios-native.mov
iOS: mWeb Safari
ios-web.mov
MacOS: Chrome / Safari
web-chrome.mov
MacOS: Desktop