-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Improve performance of inbox page loading #76012
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
Changes from 20 commits
cd961e4
4360b72
9062688
0b40de3
ef7241d
66a4661
5ac39ed
72e13e0
5775107
49b4f8e
2b2cbaa
221c028
66553b2
2afc2a2
8c9e056
b072b5c
427bcf6
188a321
be19b55
84532e5
b56f8c9
cadb9e9
9c637d2
5ddb9ac
f917245
7ca5741
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sorry, my work got slightly delayed because I was busy with the biometrics tasks. Based on what Kuba shared with me, I prepared a simple solution for the Deleted Report Screen. However, it does have some drawbacks. 1. Solution based on Jakub’s proposalWe add a state variable to the 2. Persisting this state in OnyxThis would solve part of the problem, but if the user has never visited the report and it gets deleted by someone else, they will still see the notFound page. 3. Listening to all Report changes and storing missing IDs in OnyxWe could monitor all changes in Reports and store IDs of missing ones under a new Onyx key. 4. Storing deleted report IDs on the backendThis is the most complex solution to implement but provides the best UX, as it lets us reliably detect all deleted reports. 5. Do nothingThe PR already improves performance, and the notFound page already informs the user that something may have happened or that the report was deleted. I kept the explanation brief to avoid overwhelming you with unnecessary details at this stage.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dubielzyk-expensify What do you think about this?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't speak to the technical side, but I like that we now have a specific deleted empty state instead of not found. cc @trjExpensify and @dannymcclain for thoughts though.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dubielzyk-expensify From what I've noticed in production, depending on the situation, we either redirect immediately or display the "not found" screen:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agree - I bet we could riff on that illustration but I dig using the trashcan, and I think having a dedicated "this was deleted" instead of "it's not here" is so much more clear.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand your point of view and I agree with it. Unfortunately, every solution that would allow us to add this state introduces significant implementation overhead. My recommendation would be to merge this PR and create a separate issue dedicated to the Regarding the current PR, it introduces a small change compared with the production version: In production, navigating back to a deleted report sometimes redirects the user to another chat. This PR instead displays a What do you think about this approach?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds okay to me, but keen to hear what @Expensify/product thinks
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed in slack and the descision is to keep this super simple now and just show the classic not found page https://expensify.slack.com/archives/C05LX9D6E07/p1767656608792629 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -164,7 +164,6 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
| const reportActionIDFromRoute = route?.params?.reportActionID; | ||
| const isFocused = useIsFocused(); | ||
| const prevIsFocused = usePrevious(isFocused); | ||
| const firstRenderRef = useRef(true); | ||
| const [firstRender, setFirstRender] = useState(true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain the reason why use state instead of ref?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks to replacing useRef with useState, we were able to enable the React Compiler. The benefit we previously got from useRef was minimal anyway, because we were already using setState below, which triggered a re-render. Now that ref is no longer used inside the effect, the React Compiler can properly memoize the component, resulting in better performance.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though this ref was introduced 2 years ago, last usage was 10 months ago.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This bug no longer occurs because newly created expenses are now displayed in the new report view, and in that view the issue does not appear when removing expense. Screen.Recording.2025-12-04.at.10.20.34.mov
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Screen.Recording.2025-12-04.at.10.48.17.mov |
||
| const isSkippingOpenReport = useRef(false); | ||
| const flatListRef = useRef<FlatList>(null); | ||
|
|
@@ -311,8 +310,6 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
| const [isBannerVisible, setIsBannerVisible] = useState(true); | ||
| const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({}); | ||
|
|
||
| const wasReportAccessibleRef = useRef(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm already checking whether this doesn't reproduce. Thanks for finding it, because I have a feeling it might show up, since I think I had checked different cases.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m not sure what the correct behavior should be here. If we try to navigate to a deleted report, should we see the “Oops” screen or not?
The message on the page also seems appropriate, since it informs us that we’re trying to navigate to an expense that has already been deleted. Screen.Recording.2025-12-04.at.10.50.51.movIt seems to me that it would be best if the @Expensify/design team spoke up about what the correct flow should be here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the @Expensify/design team decides it should be an upss page, I’ll keep it. If not, I’ll add navigation wherever they think it’s appropriate in that situation.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure exactly where I would expect to land when I went back to the inbox, but it wouldn't be a not found screen. @dubielzyk-expensify @Expensify/product-pr do y'all have any opinions on where a user should land here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From a technical point of view, getting information about whether or not an expense is deleted is the first step to both navigating and showing information that an expense has been deleted. So I can do what you see fit.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My take would be that and basically just swap the text and illustration to this:
But curious to hear if @dannymcclain agrees. And @Expensify/product-pr 's take too
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that text and illustration is much better than a generic one when we know the absolute reason. The cloud guy is in a ton of places, and it's somewhat not as helpful when it's ambiguously positioned (i.e "it could be this, or that, maybe even this!"). That said, I generally don't really like the pattern of returning to an error screen if it can be avoided by removing the deleted report from the stack. So I think my preference would be something that accommodates that, really.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm into that empty state because like Tom mentioned it's much more clear. But I definitely see his point about us purposely landing someone on an "error" page. I'm just having a hard time thinking of a sensible place to land someone.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I currently have a higher-priority task related to SAML. As soon as I’m done with it (most likely on Tuesday), I’ll take care of the fixes for the new empty state |
||
|
|
||
| const viewportOffsetTop = useViewportOffsetTop(); | ||
|
|
||
| const {reportPendingAction, reportErrors} = getReportOfflinePendingActionAndErrors(report); | ||
|
|
@@ -370,14 +367,6 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
| hideEmojiPicker(true); | ||
| }, [prevIsFocused, isFocused]); | ||
|
|
||
| useEffect(() => { | ||
| if (!report?.reportID) { | ||
| wasReportAccessibleRef.current = false; | ||
| return; | ||
| } | ||
| wasReportAccessibleRef.current = true; | ||
| }, [report?.reportID]); | ||
|
|
||
| const backTo = route?.params?.backTo as string; | ||
| const onBackButtonPress = useCallback( | ||
| (prioritizeBackTo = false) => { | ||
|
|
@@ -410,40 +399,55 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
| [isInSidePanel, backTo, isInNarrowPaneModal, closeSidePanel], | ||
| ); | ||
|
|
||
| let headerView = ( | ||
| <HeaderView | ||
| reportID={reportIDFromRoute} | ||
| onNavigationMenuButtonClicked={onBackButtonPress} | ||
| report={report} | ||
| parentReportAction={parentReportAction} | ||
| shouldUseNarrowLayout={shouldUseNarrowLayout} | ||
| isInSidePanel={isInSidePanel} | ||
| /> | ||
| ); | ||
| const headerView = useMemo(() => { | ||
| if (isTransactionThreadView) { | ||
| return ( | ||
| <MoneyRequestHeader | ||
| report={report} | ||
| policy={policy} | ||
| parentReportAction={parentReportAction} | ||
| onBackButtonPress={onBackButtonPress} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| if (isTransactionThreadView) { | ||
| headerView = ( | ||
| <MoneyRequestHeader | ||
| report={report} | ||
| policy={policy} | ||
| parentReportAction={parentReportAction} | ||
| onBackButtonPress={onBackButtonPress} | ||
| /> | ||
| ); | ||
| } | ||
| if (isMoneyRequestOrInvoiceReport) { | ||
| return ( | ||
| <MoneyReportHeader | ||
| report={report} | ||
| policy={policy} | ||
| transactionThreadReportID={transactionThreadReportID} | ||
| isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} | ||
| reportActions={reportActions} | ||
| onBackButtonPress={onBackButtonPress} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| if (isMoneyRequestOrInvoiceReport) { | ||
| headerView = ( | ||
| <MoneyReportHeader | ||
| return ( | ||
| <HeaderView | ||
| reportID={reportIDFromRoute} | ||
| onNavigationMenuButtonClicked={onBackButtonPress} | ||
| report={report} | ||
| policy={policy} | ||
| transactionThreadReportID={transactionThreadReportID} | ||
| isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} | ||
| reportActions={reportActions} | ||
| onBackButtonPress={onBackButtonPress} | ||
| parentReportAction={parentReportAction} | ||
| shouldUseNarrowLayout={shouldUseNarrowLayout} | ||
|
Comment on lines
+449
to
+453
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When ReportScreen is rendered in the side panel, HeaderView relies on Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good finding. Report screen can also be in side panel so this should be tested too.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| isInSidePanel={isInSidePanel} | ||
| /> | ||
| ); | ||
| } | ||
| }, [ | ||
| isTransactionThreadView, | ||
| isMoneyRequestOrInvoiceReport, | ||
| report, | ||
| policy, | ||
| parentReportAction, | ||
| onBackButtonPress, | ||
| transactionThreadReportID, | ||
| reportMetadata.isLoadingInitialReportActions, | ||
| reportActions, | ||
| reportIDFromRoute, | ||
| shouldUseNarrowLayout, | ||
| isInSidePanel, | ||
| ]); | ||
|
|
||
| useEffect(() => { | ||
| if (!transactionThreadReportID || !route?.params?.reportActionID || !isOneTransactionThread(childReport, report, linkedAction)) { | ||
|
|
@@ -462,7 +466,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
|
|
||
| const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); | ||
|
|
||
| const lastReportActionIDFromRoute = usePrevious(!firstRenderRef.current ? reportActionIDFromRoute : undefined); | ||
| const lastReportActionIDFromRoute = usePrevious(!firstRender ? reportActionIDFromRoute : undefined); | ||
|
|
||
| const [isNavigatingToDeletedAction, setIsNavigatingToDeletedAction] = useState(false); | ||
|
|
||
|
|
@@ -498,25 +502,34 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
| const currentReportIDFormRoute = route.params?.reportID; | ||
|
|
||
| // eslint-disable-next-line rulesdir/no-negated-variables | ||
| const shouldShowNotFoundPage = useMemo( | ||
| (): boolean => { | ||
| if (shouldShowNotFoundLinkedAction) { | ||
| return true; | ||
| } | ||
| const shouldShowNotFoundPage = useMemo((): boolean => { | ||
| const isLoading = !!isLoadingApp || !!isLoadingReportData || !!reportMetadata?.isLoadingInitialReportActions; | ||
| const reportExists = !!reportID || !!isOptimisticDelete || !!userLeavingStatus; | ||
| const isInvalidReportPath = !!currentReportIDFormRoute && !isValidReportIDFromPath(currentReportIDFormRoute); | ||
|
|
||
| if (isLoadingApp !== false) { | ||
| return false; | ||
| } | ||
| if (shouldShowNotFoundLinkedAction) { | ||
| return true; | ||
| } | ||
|
|
||
| if (!wasReportAccessibleRef.current && !firstRenderRef.current && !reportID && !isOptimisticDelete && !reportMetadata?.isLoadingInitialReportActions && !userLeavingStatus) { | ||
| return true; | ||
| } | ||
| if (isLoading) { | ||
| return false; | ||
| } | ||
|
|
||
| return !!currentReportIDFormRoute && !isValidReportIDFromPath(currentReportIDFormRoute); | ||
| }, | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [firstRender, shouldShowNotFoundLinkedAction, reportID, isOptimisticDelete, reportMetadata?.isLoadingInitialReportActions, userLeavingStatus, currentReportIDFormRoute], | ||
|
Comment on lines
-533
to
-534
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An incomplete dependency array was created in this PR #56819. Tests
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add that test to the PR description please
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sumo-slonik It might also be worth including information in the description that the redirect has been removed from the non-existent report and that we're now showing the "not found" state. |
||
| ); | ||
| if (!reportExists) { | ||
| return true; | ||
mountiny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return isInvalidReportPath; | ||
| }, [ | ||
| shouldShowNotFoundLinkedAction, | ||
| isLoadingApp, | ||
| isLoadingReportData, | ||
| reportMetadata?.isLoadingInitialReportActions, | ||
| reportID, | ||
| isOptimisticDelete, | ||
| userLeavingStatus, | ||
| currentReportIDFormRoute, | ||
| ]); | ||
|
|
||
| const createOneTransactionThreadReport = useCallback(() => { | ||
| const currentReportTransaction = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); | ||
|
|
@@ -693,8 +706,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr | |
|
|
||
| useEffect(() => { | ||
| // We don't want this effect to run on the first render. | ||
| if (firstRenderRef.current) { | ||
| firstRenderRef.current = false; | ||
| if (firstRender) { | ||
| setFirstRender(false); | ||
| return; | ||
| } | ||
|
|
||





Uh oh!
There was an error while loading. Please reload this page.