Fix cache for missing keys and empty collections#401
Fix cache for missing keys and empty collections#401mountiny merged 3 commits intoExpensify:mainfrom
Conversation
4307bfe to
7b98cca
Compare
7b98cca to
2111b74
Compare
2111b74 to
ed3b96b
Compare
| if (isCollectionKey(key)) { | ||
| const allKeys = cache.getAllKeys(); | ||
|
|
||
| // It is possible we haven't loaded all keys yet so we do not know if the |
There was a problem hiding this comment.
How is it possible to not have all keys loaded yet (can you please add this to the code comment)?
There was a problem hiding this comment.
If we haven't called getAllKeys yet (from Onyx, not OnyxCache). Not sure if that can actually happen but to be safe this is prob better. I can clarify.
There was a problem hiding this comment.
Ah, I did not notice the subtle difference between getAllKeys from Onyx vs OnyxCache.
There was a problem hiding this comment.
Thinking a little more on this, can you please change allKeys to be renamed to allCacheKeys?
| expect(textComponent).not.toBeNull(); | ||
| expect(onRender).toHaveBeenCalledTimes(1); | ||
|
|
||
| // Unmount the component and mount it again. It should now render immediately. |
There was a problem hiding this comment.
Is the only way to know that it wasn't rendered immediately because the test doesn't need to add a waitForPromisesToResolve in order for the assertions to pass? If so, can you please clarify that in the comment. It's not very obvious.
There was a problem hiding this comment.
Yes, if the assertion passes right away, before waitForPromisesToResolve this means it rendered synchronously since all data is cached. I can clarify.
| expect(textComponent).not.toBeNull(); | ||
| expect(onRender).toHaveBeenCalledTimes(1); | ||
|
|
||
| // Unmount the component and mount it again. It should now render immediately. |
| const onRender = jest.fn(); | ||
| let renderResult; | ||
|
|
||
| return StorageMock.setItem(ONYX_KEYS.SIMPLE_KEY, 'simple') |
There was a problem hiding this comment.
Curious why do we need to add this key? is it just so there is something in the onyx storage?
There was a problem hiding this comment.
Yea to know if all keys are in cache we currently check if the array length is > 0 so if the storage is empty the check never works.
|
Updated! |
| if (isCollectionKey(key)) { | ||
| const allKeys = cache.getAllKeys(); | ||
|
|
||
| // It is possible we haven't loaded all keys yet so we do not know if the |
There was a problem hiding this comment.
Thinking a little more on this, can you please change allKeys to be renamed to allCacheKeys?
Details
I found 2 cases where onyx component do not properly cache their data and cause it to be fetched from storage every time it mounts, even if it mounted before.
The first case is when the key does not exist in storage. In that case we never end up calling the
getfunction and just send null to the connection. This means that we never cache that this key doesn't exist. To fix that we can manually set null in the cache for this code path.The 2nd case is when we have an empty collection. Currently
tryGetCachedValuealways returnsundefinedwhen the collection is empty, but we always have all keys available so it is safe to just return an empty collection in that case. The only case where we don't have all keys is if we never calledgetAllKeysbefore and in that casecache.getAllKeyswill be empty, so that is the only case where we need to returnundefined.Related Issues
N/A
Slack: https://expensify.slack.com/archives/C05LX9D6E07/p1696601323002289
Automated Tests
This adds 3 tests to validate caching behavior of
withOnyx. The first one validates a simple case of mounting and unmounting a component. The first time it mounts it should load data from storage and the 2nd time it should be able to synchronously render. This test already passes before these changes.The 2nd and 3rd test validate behaviors fixed in the PR, missing keys and empty collections. It makes sure that they are also properly cached on remount.
Manual Tests
I tested in expensify app by scrolling to the bottom of the chats list to make sure all data has been loaded in cache, then start profiling with react devtools and scroll back to the top of the list.
Before:
You can see a large number of small renders
They are caused by
loadingprop change, which should not happen since we already loaded that data.After:
Only a few bigger renders, seems like there is still an extra render for each chat cell because of a change to the personalDetails prop, we could investigate in a follow up.
Also manually tested the main app screens to make sure it doesn't cause any issues.
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
Web
Mobile Web - Chrome
Mobile Web - Safari
Desktop
iOS
Android