-
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 problems related to Onyx.clear and useOnyx #588
Conversation
LGTM ! |
Adding screenshots / videos... |
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.
This looks great to me, and looks like it was peer reviewed a few times in Slack 👍 👍
@tgolen or @marcaaron do either of y'all want to take a look before we merge?
Bump @tgolen @marcaaron |
Happy to review this, but I don't yet understand what problem we are solving. Can we update the description with the explanation? I took a look at the comment we linked out to and it says:
Am I reading that "maybe" correctly? Are we not sure why it happens? Do we have a better explanation somewhere? |
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.
Most of my comments are about improving the documentation for now.
lib/OnyxConnectionManager.ts
Outdated
let suffix = ''; | ||
|
||
// The current session ID is appended to the connection ID so we can have different connections | ||
// after an Onyx.clear() operation. |
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.
so we can have different connections after an Onyx.clear() operation.
Let's also say why that is important or needed?
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 moved all explanations to private sessionID: string;
, please have a look!
lib/OnyxConnectionManager.ts
Outdated
/** | ||
* Refreshes the connection manager's session ID. Used in Onyx.clear() in order to | ||
* create new connections after Onyx is cleared, instead of reusing existing connections | ||
* that can produce unexpected behavior in Onyx subscribers. |
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.
Can we get more specific? What is the "unexpected behavior"? I think including an example will help later on if someone needs to make changes here or questions whether it is still required.
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 moved all explanations to private sessionID: string;
, please have a look!
/** | ||
* If set to `false`, the connection won't be reused between other subscribers that are listening to the same Onyx key | ||
* with the same connect configurations. | ||
*/ |
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.
Can we include an example of when might we want to use this?
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.
We don't have explicit use cases yet. I decided to expose this configuration because we did the same for Onyx.connect()
(and for this one we have one use case here), but for know it's just for convenience purposes.
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.
Ok, I see now. Not really related to these changes.
lib/useOnyx.ts
Outdated
// If the cache was set for the first time, we also update the cached value and the result. | ||
const isCacheSetFirstTime = previousValueRef.current === null && hasCacheForKey; | ||
// If the cache was set for the first time or we have a pending Onyx.clear() task, we also update the cached value and the result. | ||
const isCacheSetFirstTime = previousValueRef.current === null && (hasCacheForKey || OnyxCache.hasPendingTask(TASK.CLEAR)); |
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.
What does it mean for the cache to be "set first time"? Like, as in the first time the hook runs? It makes sense to consider it "set" when it has cache value. But, can we improve the documentation to explain why we should consider the cache "set" if there is a pending clear task?
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 refactored the comments, please let me know if it's clearer now.
@marcaaron Addressed the comments and added explanations to the PR description, please let me know if it's clearer now. |
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.
These changes seem reasonable to me.
Details
This PR fixes two problems we've discovered on E/App during migration to
useOnyx
, both related toOnyx.clear
. More details here.Problem 1
Racing conditions between an
Onyx.clear()
execution anduseOnyx
subscription causesuseOnyx
to be always inloading
state and never fully resolve its first connection.This happens because we clear the cache during
Onyx.clear()
execution and if we are connecting to some key withuseOnyx
at the same time, there is no cache where the hook can look into (OnyxCache.hasCacheForKey(key)
will befalse
) and it get stuck inloading
state.This problem doesn't happen for
withOnyx
basically because the racing condition doesn't occur. This can happen by the following reasons:withOnyx
is a HOC and consequently a wrapper component, it uses React lifecycles / batching mechanisms that will contribute to a slightly rendering/execution delay that is enough to allowOnyx.clear()
finish its operation.useOnyx
hook usesuseSyncExternalStore
under the hood, which is designed to not batch its updates and execute and return as fast as possible, thus contributing to the race condition to happen.Problem 2
The cache clearing performed by
Onyx.clear()
combined with the connection reusing logic we currently have causes anuseOnyx
connection to a key that was connected before to be alwaysloading
state and never fully resolve its first connection.Specifically, if we are reusing the connection of a key that is still exists in the connection manager after an
Onyx.clear()
execution, the nextuseOnyx
to connect to that same key gets stuck in theloading
state as the cache doesn't really exist anymore, and we didn't populate it again because we are reusing the connection.When logging out a lot of Onyx connections will be removed from the connection manager as expected, but some of them persists because they don't belong to the React's lifecycle e.g. any
Onyx.connect
in our lib files, helping create the issue described above.This problem doesn't happen for
withOnyx
because the HOC doesn't reuse connections and create new ones upon every subscription, thus populating the cache again for that key and allowing the HOC to work as expected.This PR also exposes the
reuseConnection
flag inuseOnyx
API for convenience purposes (it's already exposed inOnyx.connect
).Related Issues
Expensify/App#48725
Automated Tests
Unit tests were added to cover the fixes mentioned.
Manual Tests
npm test
on E/App and assert all tests are passing.❗️You will notice that after second login on Web/mWeb, user is redirected to settings page instead of home. Please have in mind that this issue is not related to these fixes, but instead one more fix that would be necessary to be made on E/App when migrating
AuthScreens.tsx
touseOnyx
.❗️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
Android: Native
Screen.Recording.2024-10-22.at.19.26.04-compressed.mov
Android: mWeb Chrome
Screen.Recording.2024-10-22.at.19.28.00-compressed.mov
iOS: Native
Screen.Recording.2024-10-22.at.19.37.13-compressed.mov
iOS: mWeb Safari
Screen.Recording.2024-10-22.at.19.38.33-compressed.mov
MacOS: Chrome / Safari
Screen.Recording.2024-10-22.at.19.40.12-compressed.mov
Screen.Recording.2024-10-22.at.19.41.53-compressed.mov
MacOS: Desktop
Screen.Recording.2024-10-22.at.19.45.03-compressed.mov