Skip to content

Commit 40b8400

Browse files
authored
Merge pull request Expensify#88613 from callstack-internal/perf/split-report-metadata
perf: split reportMetadata into key groups
2 parents 79cf994 + 0353d23 commit 40b8400

60 files changed

Lines changed: 426 additions & 305 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8233,6 +8233,7 @@ const CONST = {
82338233
},
82348234
},
82358235
DEFAULT_REPORT_METADATA: {isLoadingInitialReportActions: true},
8236+
DEFAULT_REPORT_LOADING_STATE: {isLoadingInitialReportActions: true},
82368237
UPGRADE_PATHS: {
82378238
CATEGORIES: 'categories',
82388239
REPORTS: 'reports',

src/ONYXKEYS.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,10 @@ const ONYXKEYS = {
484484
// Stores last visited path
485485
LAST_VISITED_PATH: 'lastVisitedPath',
486486

487+
/** Map of reportID → DB-formatted timestamp for when the user last visited each report.
488+
* Only consumed by `findLastAccessedReport` / `getMostRecentlyVisitedReport` for navigation fallbacks. */
489+
REPORT_LAST_VISIT_TIMES: 'reportLastVisitTimes',
490+
487491
// Stores the recently used report fields
488492
RECENTLY_USED_REPORT_FIELDS: 'recentlyUsedReportFields',
489493

@@ -732,10 +736,17 @@ const ONYXKEYS = {
732736
REPORT: 'report_',
733737
REPORT_NAME_VALUE_PAIRS: 'reportNameValuePairs_',
734738
REPORT_DRAFT: 'reportDraft_',
735-
// REPORT_METADATA is a perf optimization used to hold loading states (isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions).
736-
// A lot of components are connected to the Report entity and do not care about the actions. Setting the loading state
737-
// directly on the report caused a lot of unnecessary re-renders
739+
// REPORT_METADATA holds report-level business state that is NOT the report itself
740+
// (optimistic flag, pending chat members, report-level errors, DEW pendingExpenseAction).
741+
// Loading flags / pagination cursors / last-visit timestamp live in dedicated
742+
// keys below (REPORT_LOADING_STATE, REPORT_PAGINATION_STATE, REPORT_LAST_VISIT_TIMES)
743+
// so they don't ripple to every subscriber of the report's business state.
738744
REPORT_METADATA: 'reportMetadata_',
745+
/** Session-scoped loading/error flags for a report's action list.
746+
* Registered as RAM-only in `setup/index.ts`. */
747+
RAM_ONLY_REPORT_LOADING_STATE: 'reportLoadingState_',
748+
/** Pagination cursors for a report's action list. */
749+
REPORT_PAGINATION_STATE: 'reportPaginationState_',
739750
REPORT_ACTIONS: 'reportActions_',
740751
REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_',
741752
REPORT_ACTIONS_PAGES: 'reportActionsPages_',
@@ -1262,6 +1273,8 @@ type OnyxCollectionValuesMapping = {
12621273
[ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: OnyxTypes.ReportNameValuePairs;
12631274
[ONYXKEYS.COLLECTION.REPORT_DRAFT]: OnyxTypes.Report;
12641275
[ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata;
1276+
[ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE]: OnyxTypes.ReportLoadingState;
1277+
[ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE]: OnyxTypes.ReportPaginationState;
12651278
[ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions;
12661279
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts;
12671280
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages;
@@ -1457,6 +1470,7 @@ type OnyxValuesMapping = {
14571470
[ONYXKEYS.ONBOARDING_LAST_VISITED_PATH]: string;
14581471
[ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS]: boolean;
14591472
[ONYXKEYS.LAST_VISITED_PATH]: string | undefined;
1473+
[ONYXKEYS.REPORT_LAST_VISIT_TIMES]: OnyxTypes.ReportLastVisitTimes;
14601474
[ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
14611475
[ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED]: boolean;
14621476
[ONYXKEYS.SUPPORTAL_PERMISSION_DENIED]: OnyxTypes.SupportalPermissionDenied | null;

src/components/MoneyReportHeaderNextStep.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ type MoneyReportHeaderNextStepProps = {
1616
*/
1717
function MoneyReportHeaderNextStep({reportID}: MoneyReportHeaderNextStepProps) {
1818
const {isOffline} = useNetwork();
19-
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`);
20-
const isLoadingInitialReportActions = reportMetadata?.isLoadingInitialReportActions;
19+
const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportID}`);
20+
const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions;
2121
const optimisticNextStep = useOptimisticNextStep(reportID);
2222

2323
const showNextStepBar = !!optimisticNextStep && (('message' in optimisticNextStep && !!optimisticNextStep.message?.length) || 'messageKey' in optimisticNextStep);

src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
100100
// report is guaranteed to exist — callers only render this component when report is loaded
101101
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`) as unknown as [OnyxTypes.Report];
102102
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`);
103-
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`);
103+
const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`);
104+
const [reportPaginationState] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE}${reportIDFromRoute}`);
104105
const reportID = report?.reportID;
105106

106107
const {reportActions: unfilteredReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, route?.params?.reportActionID);
@@ -116,8 +117,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
116117
() => Object.values(allReportTransactions ?? {}).some((transaction) => transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
117118
[allReportTransactions],
118119
);
119-
const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, reportTransactions);
120-
const showReportActionsLoadingState = reportMetadata?.isLoadingInitialReportActions && !reportMetadata?.hasOnceLoadedReportActions;
120+
const newTransactions = useNewTransactions(reportLoadingState?.hasOnceLoadedReportActions, reportTransactions);
121+
const showReportActionsLoadingState = reportLoadingState?.isLoadingInitialReportActions && !reportLoadingState?.hasOnceLoadedReportActions;
121122
const reportTransactionIDs = useMemo(() => transactions.map((transaction) => transaction.transactionID), [transactions]);
122123
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`);
123124

@@ -225,23 +226,31 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
225226
transactionThreadReport,
226227
hasOlderActions,
227228
hasNewerActions,
228-
newestFetchedReportActionID: reportMetadata?.newestFetchedReportActionID,
229+
newestFetchedReportActionID: reportPaginationState?.newestFetchedReportActionID,
229230
});
230231

231-
const hasFinishedInitialLoad = reportMetadata?.isLoadingInitialReportActions === false;
232+
const hasFinishedInitialLoad = reportLoadingState?.isLoadingInitialReportActions === false;
232233
const prevNewestFetchedIDRef = useRef<string | undefined>(undefined);
233234
useEffect(() => {
234-
if (hasFinishedInitialLoad && hasNewerActions && reportActions.length > 0 && !isOffline && !reportMetadata?.isLoadingNewerReportActions) {
235+
if (hasFinishedInitialLoad && hasNewerActions && reportActions.length > 0 && !isOffline && !reportLoadingState?.isLoadingNewerReportActions) {
235236
// Safety guard: if the cursor hasn't advanced since the last call, the server
236237
// isn't returning new data. Stop to prevent an infinite request loop.
237-
const currentCursor = reportMetadata?.newestFetchedReportActionID;
238+
const currentCursor = reportPaginationState?.newestFetchedReportActionID;
238239
if (prevNewestFetchedIDRef.current !== undefined && prevNewestFetchedIDRef.current === currentCursor) {
239240
return;
240241
}
241242
prevNewestFetchedIDRef.current = currentCursor;
242243
loadNewerChats(false);
243244
}
244-
}, [hasFinishedInitialLoad, reportActions.length, hasNewerActions, isOffline, reportMetadata?.isLoadingNewerReportActions, reportMetadata?.newestFetchedReportActionID, loadNewerChats]);
245+
}, [
246+
hasFinishedInitialLoad,
247+
reportActions.length,
248+
hasNewerActions,
249+
isOffline,
250+
reportLoadingState?.isLoadingNewerReportActions,
251+
reportPaginationState?.newestFetchedReportActionID,
252+
loadNewerChats,
253+
]);
245254

246255
// Backfill loop: the backend prioritizes IOU actions in OpenReport/GetNewerActions for money
247256
// request reports, which can leave non-IOU chat messages in a gap between the IOU-biased cursor
@@ -257,18 +266,18 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
257266
isBackfillingRef.current = false;
258267
}
259268
useEffect(() => {
260-
if (!hasFinishedInitialLoad || isOffline || hasNewerActions || reportMetadata?.isLoadingNewerReportActions || reportMetadata?.isLoadingOlderReportActions) {
269+
if (!hasFinishedInitialLoad || isOffline || hasNewerActions || reportLoadingState?.isLoadingNewerReportActions || reportLoadingState?.isLoadingOlderReportActions) {
261270
return;
262271
}
263272

264273
if (!isBackfillingRef.current) {
265274
const hasIOUActions = reportActions.some((action) => isMoneyRequestAction(action));
266-
if (!hasIOUActions || reportActions.length < BACKFILL_MIN_ACTIONS_THRESHOLD || !reportMetadata?.newestFetchedReportActionID) {
275+
if (!hasIOUActions || reportActions.length < BACKFILL_MIN_ACTIONS_THRESHOLD || !reportPaginationState?.newestFetchedReportActionID) {
267276
return;
268277
}
269278
}
270279

271-
const cursor = isBackfillingRef.current ? reportMetadata?.oldestFetchedReportActionID : reportMetadata?.newestFetchedReportActionID;
280+
const cursor = isBackfillingRef.current ? reportPaginationState?.oldestFetchedReportActionID : reportPaginationState?.newestFetchedReportActionID;
272281
if (!cursor) {
273282
return;
274283
}
@@ -287,10 +296,10 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
287296
hasFinishedInitialLoad,
288297
isOffline,
289298
hasNewerActions,
290-
reportMetadata?.isLoadingNewerReportActions,
291-
reportMetadata?.isLoadingOlderReportActions,
292-
reportMetadata?.newestFetchedReportActionID,
293-
reportMetadata?.oldestFetchedReportActionID,
299+
reportLoadingState?.isLoadingNewerReportActions,
300+
reportLoadingState?.isLoadingOlderReportActions,
301+
reportPaginationState?.newestFetchedReportActionID,
302+
reportPaginationState?.oldestFetchedReportActionID,
294303
reportActions,
295304
reportID,
296305
]);
@@ -357,7 +366,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
357366
// To handle this, we use the 'referrer' parameter to check if the current navigation is triggered from a notification.
358367
const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION;
359368
if ((isVisible || isFromNotification) && scrollingVerticalBottomOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) {
360-
readNewestAction(report?.reportID, !!reportMetadata?.hasOnceLoadedReportActions);
369+
readNewestAction(report?.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
361370
if (isFromNotification) {
362371
Navigation.setParams({referrer: undefined});
363372
}
@@ -366,7 +375,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
366375
}
367376
}
368377
// eslint-disable-next-line react-hooks/exhaustive-deps
369-
}, [report?.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report?.reportID, isVisible, reportMetadata?.hasOnceLoadedReportActions]);
378+
}, [report?.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report?.reportID, isVisible, reportLoadingState?.hasOnceLoadedReportActions]);
370379

371380
useEffect(() => {
372381
if (!isVisible || !isFocused) {
@@ -390,15 +399,15 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
390399
return;
391400
}
392401

393-
readNewestAction(report?.reportID, !!reportMetadata?.hasOnceLoadedReportActions);
402+
readNewestAction(report?.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
394403
userActiveSince.current = DateUtils.getDBTime();
395404

396405
// This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility
397406
// is changed to visible(meaning user switched to app/web, while user was previously using different tab or application).
398407
// We will mark the report as read in the above case which marks the LHN report item as read while showing the new message
399408
// marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web.
400409
// eslint-disable-next-line react-hooks/exhaustive-deps
401-
}, [isFocused, isVisible, reportMetadata?.hasOnceLoadedReportActions]);
410+
}, [isFocused, isVisible, reportLoadingState?.hasOnceLoadedReportActions]);
402411

403412
/**
404413
* The index of the earliest message that was received while offline
@@ -448,7 +457,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
448457
// We additionally track the top offset to be able to scroll to the new transaction when it's added
449458
scrollingVerticalTopOffset.current = contentOffset.y;
450459
},
451-
hasOnceLoadedReportActions: !!reportMetadata?.hasOnceLoadedReportActions,
460+
hasOnceLoadedReportActions: !!reportLoadingState?.hasOnceLoadedReportActions,
452461
});
453462

454463
useScrollToEndOnNewMessageReceived({
@@ -620,8 +629,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
620629

621630
reportScrollManager.scrollToEnd();
622631
readActionSkipped.current = false;
623-
readNewestAction(report?.reportID, !!reportMetadata?.hasOnceLoadedReportActions);
624-
}, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report?.reportID, reportMetadata?.hasOnceLoadedReportActions, introSelected, betas]);
632+
readNewestAction(report?.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
633+
}, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report?.reportID, reportLoadingState?.hasOnceLoadedReportActions, introSelected, betas]);
625634

626635
const scrollToNewTransaction = useCallback(
627636
(pageY: number) => {

src/components/MoneyRequestReportView/MoneyRequestReportView.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ type MoneyRequestReportViewProps = {
4747
/** The report */
4848
report: OnyxEntry<OnyxTypes.Report>;
4949

50-
/** Metadata for report */
51-
reportMetadata: OnyxEntry<OnyxTypes.ReportMetadata>;
50+
/** Loading state for report */
51+
reportLoadingState: OnyxEntry<OnyxTypes.ReportLoadingState>;
5252

5353
/** Whether Report footer (that includes Composer) should be displayed */
5454
shouldDisplayReportFooter: boolean;
@@ -104,7 +104,7 @@ function InitialLoadingSkeleton({styles, onLayout, reasonAttributes}: {styles: T
104104
);
105105
}
106106

107-
function MoneyRequestReportView({report, reportMetadata, shouldDisplayReportFooter, backToRoute, onLayout}: MoneyRequestReportViewProps) {
107+
function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReportFooter, backToRoute, onLayout}: MoneyRequestReportViewProps) {
108108
const styles = useThemeStyles();
109109
const {isOffline} = useNetwork();
110110

@@ -141,7 +141,7 @@ function MoneyRequestReportView({report, reportMetadata, shouldDisplayReportFoot
141141
const reportTransactionIDs = visibleTransactions.map((transaction) => transaction.transactionID);
142142
const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);
143143

144-
const isLoadingInitialReportActions = reportMetadata?.isLoadingInitialReportActions;
144+
const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions;
145145
const dismissReportCreationError = useCallback(() => {
146146
goBackFromSearchMoneyRequest();
147147
// eslint-disable-next-line @typescript-eslint/no-deprecated
@@ -154,7 +154,7 @@ function MoneyRequestReportView({report, reportMetadata, shouldDisplayReportFoot
154154

155155
// Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render
156156
// We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate
157-
const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata, isOffline);
157+
const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportLoadingState, isOffline);
158158

159159
const shouldShowOpenReportLoadingSkeleton = !!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions;
160160

src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ function MoneyRequestReportPreviewContent({
108108
originalReportID,
109109
}: MoneyRequestReportPreviewContentProps) {
110110
const [chatReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`);
111+
const [chatReportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`);
111112

112113
const [isTransitionPending, setIsTransitionPending] = useState(() => {
113114
const pending = getPendingSubmitFollowUpAction();
@@ -126,7 +127,7 @@ function MoneyRequestReportPreviewContent({
126127
return () => handle.cancel();
127128
}, [isTransitionPending]),
128129
);
129-
const shouldShowLoading = !chatReportMetadata?.hasOnceLoadedReportActions && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
130+
const shouldShowLoading = !chatReportLoadingState?.hasOnceLoadedReportActions && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
130131
// `hasOnceLoadedReportActions` becomes true before transactions populate fully,
131132
// so we defer the loading state update to ensure transactions are loaded
132133
const shouldShowLoadingDeferred = useDeferredValue(shouldShowLoading);
@@ -141,7 +142,7 @@ function MoneyRequestReportPreviewContent({
141142
const shouldShowPreviewLoading = isTransitionPending || shouldShowLoading || shouldShowLoadingDeferred || (!currentWidth && !shouldShowPreviewPlaceholder);
142143
const skeletonReasonAttributes: SkeletonSpanReasonAttributes = {
143144
context: 'MoneyRequestReportPreviewContent',
144-
hasOnceLoadedReportActions: chatReportMetadata?.hasOnceLoadedReportActions,
145+
hasOnceLoadedReportActions: chatReportLoadingState?.hasOnceLoadedReportActions,
145146
isTransactionsEmpty: transactions.length === 0,
146147
isOptimisticReport: chatReportMetadata?.isOptimisticReport,
147148
};

src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ function MoneyRequestReportPreview({
118118
Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()}));
119119
}
120120
}, [iouReportID, isSmallScreenWidth]);
121-
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`, {
121+
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`, {
122122
selector: hasOnceLoadedReportActionsSelector,
123123
});
124124
const newTransactions = useNewTransactions(hasOnceLoadedReportActions, transactions);

src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function ExpenseReportListItem<TItem extends ListItem>({
6161
const {translate} = useLocalize();
6262
const {isLargeScreenWidth} = useResponsiveLayout();
6363
const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchStateContext();
64-
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {selector: isActionLoadingSelector});
64+
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`, {selector: isActionLoadingSelector});
6565
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
6666
const currentUserDetails = useCurrentUserPersonalDetails();
6767

0 commit comments

Comments
 (0)