Skip to content
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

Merged
merged 7 commits into from
Oct 31, 2024

Conversation

fabioh8010
Copy link
Contributor

@fabioh8010 fabioh8010 commented Oct 8, 2024

Details

This PR fixes two problems we've discovered on E/App during migration to useOnyx, both related to Onyx.clear. More details here.

Problem 1

Racing conditions between an Onyx.clear() execution and useOnyx subscription causes useOnyx to be always in loading 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 with useOnyx at the same time, there is no cache where the hook can look into (OnyxCache.hasCacheForKey(key) will be false) and it get stuck in loading state.

This problem doesn't happen for withOnyx basically because the racing condition doesn't occur. This can happen by the following reasons:

  • As 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 allow Onyx.clear() finish its operation.
  • The useOnyx hook uses useSyncExternalStore 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 an useOnyx connection to a key that was connected before to be always loading 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 next useOnyx to connect to that same key gets stuck in the loading 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 in useOnyx API for convenience purposes (it's already exposed in Onyx.connect).

Related Issues

Expensify/App#48725

Automated Tests

Unit tests were added to cover the fixes mentioned.

Manual Tests

  1. Checkout this branch on E/App.
  2. Link the changes of this Onyx PR branch to E/App.
  3. Run npm test on E/App and assert all tests are passing.
  4. Login with a user, logout and login with the same user again, assert the whole process works as expected.

❗️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 to useOnyx.❗️

Author Checklist

  • I linked the correct issue in the ### Related Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android / native
    • Android / Chrome
    • iOS / native
    • iOS / Safari
    • MacOS / Chrome / Safari
    • MacOS / Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. myBool && <MyComponent />.
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • If we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR author checklist, including those that don't apply to this PR.

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

@hannojg
Copy link
Contributor

hannojg commented Oct 21, 2024

LGTM !

@fabioh8010 fabioh8010 changed the title [WIP] Bugfix/48725 v2 Fix problems related to Onyx.clear and useOnyx Oct 22, 2024
@fabioh8010
Copy link
Contributor Author

Adding screenshots / videos...

@fabioh8010 fabioh8010 marked this pull request as ready for review October 22, 2024 20:17
@fabioh8010 fabioh8010 requested a review from a team as a code owner October 22, 2024 20:17
@melvin-bot melvin-bot bot requested review from Beamanator and removed request for a team October 22, 2024 20:17
Beamanator
Beamanator previously approved these changes Oct 22, 2024
Copy link
Contributor

@Beamanator Beamanator left a 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?

@fabioh8010
Copy link
Contributor Author

Bump @tgolen @marcaaron

@marcaaron
Copy link
Contributor

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:

From what I've seen this problem doesn't happen for withOnyx basically because the racing condition doesn't occur (maybe because it's a component and it's slightly slower during initialisation?).

Am I reading that "maybe" correctly? Are we not sure why it happens? Do we have a better explanation somewhere?

Copy link
Contributor

@marcaaron marcaaron left a 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 Show resolved Hide resolved
let suffix = '';

// The current session ID is appended to the connection ID so we can have different connections
// after an Onyx.clear() operation.
Copy link
Contributor

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?

Copy link
Contributor Author

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!

/**
* 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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.
*/
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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));
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@fabioh8010
Copy link
Contributor Author

@marcaaron Addressed the comments and added explanations to the PR description, please let me know if it's clearer now.

Copy link
Contributor

@marcaaron marcaaron left a 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.

@marcaaron marcaaron merged commit c05c014 into Expensify:main Oct 31, 2024
5 checks passed
@os-botify
Copy link
Contributor

os-botify bot commented Oct 31, 2024

🚀 Published to npm in 2.0.78 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants