Skip to content

Conversation

@SzymczakJ
Copy link
Contributor

@SzymczakJ SzymczakJ commented Jul 21, 2025

Explanation of Change

Fixed Issues

$ #66799
PROPOSAL:

Tests

  1. On account with at least one expense reports.
  2. Clear cache and restart
  3. go to Reports > Expense Reports
  4. enter one of the reports
  5. Make sure that old transactions are not highlighted
  6. Add a transaction to report(with plus button) and make sure it is highlighted.
  7. Enter and empty report and add few unreported expenses, then make sure they are highlighted.

Offline tests

QA Steps

Same as tests.

PR Author Checklist

  • I linked the correct issue in the ### Fixed 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 added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • 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: mWeb Chrome
    • iOS: Native
    • iOS: mWeb 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 verified there are no new alerts related to the canBeMissing param for useOnyx
  • 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 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 any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • 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.ts 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 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 a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • 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 PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • 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.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2025-07-21.at.16.19.09.mov
MacOS: Desktop

@SzymczakJ SzymczakJ requested a review from a team as a code owner July 21, 2025 14:59
@melvin-bot melvin-bot bot requested a review from Pujan92 July 21, 2025 14:59
@melvin-bot
Copy link

melvin-bot bot commented Jul 21, 2025

@Pujan92 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot bot removed the request for review from a team July 21, 2025 14:59
@mountiny
Copy link
Contributor

@SzymczakJ Since this regression came again, can you tihnk of automated way to cover this logic?

@SzymczakJ
Copy link
Contributor Author

I think it's impossible to test logic for this particular fix. The bug consisted of two things:

  1. parent of a component holding useNewTransactions was unmounting and then mounting again, which caused the hook to not skip the first rerender. The bug was present because we simply used useNewTransactions in the wrong place
  2. the useEffect using requestAnimationFrame was run before the onyx could successfully apply the changes. Now with the new setTimeout implementation, the tests already cover this case.

In other words, the existing tests cover everything that can be reasonably tested

@mountiny
Copy link
Contributor

Should we close this one now?

@SzymczakJ
Copy link
Contributor Author

I just managed to reproduce this bug, so not closing 😄

});
setTimeout(() => {
skipFirstTransactionsChange.current = false;
}, 1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need setTimeout here? Isn't requestAnimationFrame enough here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid using set timeout

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this just seems like a workaround

Copy link
Contributor Author

@SzymczakJ SzymczakJ Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both setTimeout and requestAnimationFrame + new Promise are workarounds for Onyx which is not merging results of OpenReport API call in one render, but in a couple of consecutive renders. This Onyx behaviour makes so, that loading state is set to false in one render and data is set in the next render(or even later), which can cause bugs if we don't wait for it.
We have no guarantee that the requestAnimationFrame + new Promise will for 100% ensure that the Onyx command will be fully merged.
On the other hand setTimeout option is much safer, just because it waits for a longer period of time.
So in conclusion, they both are a nasty workarounds, but if we don't want this bug to come back in the future(because Onyx behaviour changes a bit), we should stick with the setTimeout.
WDYT @mountiny @Pujan92

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SzymczakJ Can you think of ways that are not workarounds? or redesign this logic so we do not have to rely on workaround like this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'm passing this to the team to investigate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you watch report and reportMetadata objects you can spot that reportMetadata is set as loaded at least one render, before the object is hydrated.

@SzymczakJ I've read through all of the information you provided, and I'm not sure I understand what you mean.

In the SearchMoneyRequestReportPage the report and reportMetadata are two different values which are stored under two different onyx keys:
report -> ${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}
reportMetadata -> ${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}

const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true, canBeMissing: true});
const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {canBeMissing: true, allowStaleData: true});

They are loading independently. Besides that the reportMetadata has the default value.

If you want to control the report value loading from onyx, you can do it in the next way:

const [report, reportResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true, canBeMissing: true});
const isReportLoadingFromOnyx = isLoadingOnyxValue(reportResult);

@SzymczakJ Let me know if I misunderstood anything 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they are two separate values, but they are both coming from OpenReport API call. ReportMetadata in successData and report from the BE. I also can't use const [report, reportResult], because in this case the Onyx is empty, so it doesn't load from Onyx, but from the BE.

My main problem is that these two result of the same API call are applied in different renders(ReportMetadata first) which causes my loading state to hide even though it doesn't have the report yet.

Do you think it can be fixed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let me double-check it all!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main problem is that these two result of the same API call are applied in different renders(ReportMetadata first) which causes my loading state to hide even though it doesn't have the report yet.

I think I can reproduce it, the successData is applied a little bit earlier than the data returned from the API. I'm investigating why it happens

Copy link
Contributor

@mountiny mountiny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good, but the setTimeout seems like hack

});
setTimeout(() => {
skipFirstTransactionsChange.current = false;
}, 1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this just seems like a workaround

@VickyStash
Copy link
Contributor

@SzymczakJ During the testing, I've noticed that the initial bug (transaction highlighting on SearchMoneyRequestReportPage) isn't reproduced anymore on the main branch. I can reproduce it on the staging, but not in the main. Could you check it please?

@SzymczakJ
Copy link
Contributor Author

SzymczakJ commented Jul 30, 2025

Yes, the bug is no longer reproducible on main, but it's not reproducible only out of pure luck. The faulty Onyx behaviour stays the same.
Here's another video in which I go over the report rendering flow with the debugger:

Screen.Recording.2025-07-30.at.15.16.02.mov

In 1:08 we can see that reportMetadata is already merged and report is sitll an empty object, which then causes next render to show empty screen in 1:19. This is the problematic part.

I followed these repro steps: #66801 (comment)

@VickyStash
Copy link
Contributor

Yeah @SzymczakJ, sorry for the confusion, as I've mentioned here I was able to reproduce the issue!

What I've defined for now is that:

  1. Both API response onyx updates and successData updates are put into queuedOnyxUpdates.
  2. Then, the Onyx.update is called with queued updates:
    return Onyx.update(copyUpdates);

Example of the data passed to the Onyx.update during the flow: queuedUpdatesData.json. It contains both API response onyx updates and successData updates.

  1. Inside Onyx.update, at the end all of the updates are applied with Promise.all: https://github.com/Expensify/react-native-onyx/blob/632c00e71873d596668daf7418452155d513434c/lib/Onyx.ts#L654. I believe the root cause of the issue can be there, I'm going to do more testing tomorrow to confirm.

@VickyStash
Copy link
Contributor

I think I was able to define the root cause of the issue. In the response of OpenReport API call we have several mergecollection/merge updates to onyx:
image

Inside the Onyx.update these updates are grouped into one mergeCollection update

The inconsistency appears in notifying subscribers about the onyx update.
mergeCollectionWithPatches → scheduleNotifyCollectionSubscribers for collection updates takes much longer than
applyMerge → broadcastUpdate → scheduleSubscriberUpdate for the usual merge
ending up the merge updates received a little bit earlier by the app while the mergecollection updates are processed.

So if I try to replace mergecollection calls to simple merge -> it works faster and there is no issue.
I'll try to think about the way we can improve it!

@VickyStash
Copy link
Contributor

Hey, unfortunately, I haven't had enough time today for investigations here, as I needed to switch to the deploy blockers investigation. I'll continue here on Monday!

@SzymczakJ
Copy link
Contributor Author

FYI I'm OOO for the whole next week.

@VickyStash
Copy link
Contributor

I've defined that the batch updates logic in the onyx was working for withOnyx subscribers only. I've tried to make it work for useOnyx hook subscribers, and it fixed the issue with inconsistency in notifying subscribers about Onyx.update updates (mentioned it here).
I'm testing over the updates right now to see if there are any side effects due to the updates.

@VickyStash
Copy link
Contributor

I've defined that the batch updates logic in the onyx was working for withOnyx subscribers only. I've tried to make it work for useOnyx hook subscribers, and it fixed the issue with inconsistency in notifying subscribers about Onyx.update updates (mentioned it #66801 (comment)).
I'm testing over Expensify/react-native-onyx#669 right now to see if there are any side effects due to the updates.

UPD: I'm investigating some flaky tests in both E/App and react-native-onyx. Trying to figure out if the problem is in the tests or in the functionality updates.

@VickyStash
Copy link
Contributor

UPD: Still working on testing/checking unit tests. It turns out the app tests were also affected by the changes in v2.0.128-2.0.130, which confused me a little.
But overall, the manual testing gives good results!

@VickyStash
Copy link
Contributor

UPD: Opened Onyx PR for review!

@VickyStash
Copy link
Contributor

Hi there! I was finally unblocked for preparing the Onyx bump PR to v2.0.135! Most likely the PR will be ready for review tomorrow!

@SzymczakJ
Copy link
Contributor Author

I checked it and it seems that this bump will solve this bug right away. When the bump is merged I might refactor useNewTransactions.

@VickyStash
Copy link
Contributor

Unfortunately, one issue popped up: Expensify/react-native-onyx#669 (comment)
I'll need to double-check how many places can be affected in the app and maybe change the approach a little.

@VickyStash
Copy link
Contributor

UPD: I've started preparations for the new onyx PR, it's going to batch updates in case they come from one Onyx.update. It should fix the issue we have without side effect, I'm actively testing it

@VickyStash
Copy link
Contributor

UPD: I've opened a new Onyx PR for review 🤞

@VickyStash VickyStash mentioned this pull request Sep 4, 2025
51 tasks
@VickyStash
Copy link
Contributor

UPD: I've started preparing Onyx bump PR!
@SzymczakJ I'd really appreciate if you can check if the issue you reported is fixed in the PR

@SzymczakJ
Copy link
Contributor Author

Sure, I will take a look today.

@SzymczakJ
Copy link
Contributor Author

I can confirm that the onyx issue was fixed @VickyStash 😁

@VickyStash
Copy link
Contributor

I've opened bump PR for the review: #69872

@VickyStash
Copy link
Contributor

Unfortunately the bump PR was reverted due to the deploy blocker Expensify/App#70184 😞
I'm going to be OOO September 11 - 21 🌴 , so I'll be able to get back to it after that

@SzymczakJ
Copy link
Contributor Author

I wonder, if your PR really caused a bug or if the expense highlighting logic was based on previous useOnyx, which was not working correctly. Enjoy your holidays 😄.

@VickyStash
Copy link
Contributor

I wonder, if your PR really caused a bug or if the expense highlighting logic was based on previous useOnyx, which was not working correctly. Enjoy your holidays 😄.

I've investigated it a little yesterday and the reason was in how useEffects hooks were triggered after my update, so yeah, it most likely can be just fixed on the app side.
BUT, I have a feeling there can be a lot of places like this, so at least every DB needs to be checked

@Pujan92
Copy link
Contributor

Pujan92 commented Nov 26, 2025

@SzymczakJ can we close this PR?

@SzymczakJ SzymczakJ closed this Dec 4, 2025
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