Fix problems related to Onyx.clear and useOnyx#588
Conversation
|
LGTM ! |
|
Adding screenshots / videos... |
Beamanator
left a comment
There was a problem hiding this comment.
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? |
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.
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.
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.
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.
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.
Can we include an example of when might we want to use this?
There was a problem hiding this comment.
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.
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.
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.
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. |
marcaaron
left a comment
There was a problem hiding this comment.
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 anduseOnyxsubscription causesuseOnyxto be always inloadingstate 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 withuseOnyxat the same time, there is no cache where the hook can look into (OnyxCache.hasCacheForKey(key)will befalse) and it get stuck inloadingstate.This problem doesn't happen for
withOnyxbasically because the racing condition doesn't occur. This can happen by the following reasons:withOnyxis 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.useOnyxhook usesuseSyncExternalStoreunder 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 anuseOnyxconnection to a key that was connected before to be alwaysloadingstate 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 nextuseOnyxto connect to that same key gets stuck in theloadingstate 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.connectin our lib files, helping create the issue described above.This problem doesn't happen for
withOnyxbecause 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
reuseConnectionflag inuseOnyxAPI 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 teston 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.tsxtouseOnyx.❗️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
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