-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix cache for missing keys and empty collections #401
Conversation
4307bfe
to
7b98cca
Compare
7b98cca
to
2111b74
Compare
2111b74
to
ed3b96b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a great understanding of Onyx, so adding a couple more reviewers. The tests seem to make sense to me though.
@@ -236,6 +236,12 @@ function tryGetCachedValue(key, mapping = {}) { | |||
|
|||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I did not notice the subtle difference between getAllKeys
from Onyx vs OnyxCache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if the assertion passes right away, before waitForPromisesToResolve
this means it rendered synchronously since all data is cached. I can clarify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
const onRender = jest.fn(); | ||
let renderResult; | ||
|
||
return StorageMock.setItem(ONYX_KEYS.SIMPLE_KEY, 'simple') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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! |
@@ -236,6 +236,12 @@ function tryGetCachedValue(key, mapping = {}) { | |||
|
|||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking a little more on this, can you please change allKeys
to be renamed to allCacheKeys
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
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
get
function 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
tryGetCachedValue
always returnsundefined
when 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 calledgetAllKeys
before and in that casecache.getAllKeys
will 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
loading
prop 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 Issues
section aboveTests
sectiontoggleReport
and notonIconClick
)myBool && <MyComponent />
.STYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)/** comment above it */
this
properly so there are no scoping issues (i.e. foronClick={this.submit}
the methodthis.submit
should be bound tothis
in the constructor)this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)main
branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTest
steps.Screenshots/Videos
Web
Mobile Web - Chrome
Mobile Web - Safari
Desktop
iOS
Android