Skip to content

Commit 400319a

Browse files
committed
perf: remove PureReportActionItem and use stable report
1 parent be08a6b commit 400319a

23 files changed

Lines changed: 1316 additions & 910 deletions

src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import useReportScrollManager from '@hooks/useReportScrollManager';
2222
import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
2323
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
2424
import useScrollToEndOnNewMessageReceived from '@hooks/useScrollToEndOnNewMessageReceived';
25+
import useStableReportForReportActionItem from '@hooks/useStableReportForReportActionItem';
2526
import useThemeStyles from '@hooks/useThemeStyles';
2627
import useWindowDimensions from '@hooks/useWindowDimensions';
2728
import {isConsecutiveChronosAutomaticTimerAction} from '@libs/ChronosUtils';
@@ -51,6 +52,7 @@ import Visibility from '@libs/Visibility';
5152
import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute';
5253
import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
5354
import getInitialNumToRender from '@pages/inbox/report/getInitialNumReportActionsToRender';
55+
import ReportActionIndexContext from '@pages/inbox/report/ReportActionIndexContext';
5456
import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer';
5557
import {getUnreadMarkerReportAction} from '@pages/inbox/report/shouldDisplayNewMarkerOnReportAction';
5658
import useReportUnreadMessageScrollTracking from '@pages/inbox/report/useReportUnreadMessageScrollTracking';
@@ -99,6 +101,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
99101
// Self-subscribe to report, policy, metadata, actions, transactions
100102
// report is guaranteed to exist — callers only render this component when report is loaded
101103
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`) as unknown as [OnyxTypes.Report];
104+
// Stable reference of `report` for action items
105+
const reportStable = useStableReportForReportActionItem(report);
102106
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`);
103107
const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`);
104108
const [reportPaginationState] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE}${reportIDFromRoute}`);
@@ -559,34 +563,35 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
559563
!isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID), isOffline) &&
560564
hasNextActionMadeBySameActor(visibleReportActions, index, isOffline);
561565

562-
const originalReportID = getOriginalReportID(report?.reportID, reportAction, reportActionsObject);
566+
const originalReportID = getOriginalReportID(reportStable?.reportID, reportAction, reportActionsObject);
563567

564568
return (
565-
<ReportActionsListItemRenderer
566-
reportAction={reportAction}
567-
parentReportAction={parentReportAction}
568-
parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread}
569-
index={index}
570-
report={report}
571-
transactionThreadReport={transactionThreadReport}
572-
displayAsGroup={displayAsGroup}
573-
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
574-
shouldDisplayReplyDivider={visibleReportActions.length > 1}
575-
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
576-
shouldHideThreadDividerLine
577-
linkedReportActionID={linkedReportActionID}
578-
personalDetails={personalDetails}
579-
originalReportID={originalReportID}
580-
isReportArchived={isReportArchived}
581-
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
582-
/>
569+
<ReportActionIndexContext.Provider value={index}>
570+
<ReportActionsListItemRenderer
571+
reportAction={reportAction}
572+
parentReportAction={parentReportAction}
573+
parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread}
574+
report={reportStable}
575+
transactionThreadReport={transactionThreadReport}
576+
displayAsGroup={displayAsGroup}
577+
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
578+
shouldDisplayReplyDivider={visibleReportActions.length > 1}
579+
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
580+
shouldHideThreadDividerLine
581+
linkedReportActionID={linkedReportActionID}
582+
personalDetails={personalDetails}
583+
originalReportID={originalReportID}
584+
isReportArchived={isReportArchived}
585+
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
586+
/>
587+
</ReportActionIndexContext.Provider>
583588
);
584589
},
585590
[
586591
visibleReportActions,
587592
reportActionsObject,
588593
parentReportAction,
589-
report,
594+
reportStable,
590595
isOffline,
591596
transactionThreadReport,
592597
unreadMarkerReportActionID,

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,8 @@ function ChatListItem<TItem extends ListItem>({
7979
parentReportAction={undefined}
8080
displayAsGroup={false}
8181
shouldDisplayNewMarker={false}
82-
index={item.index ?? 0}
8382
isFirstVisibleReportAction={false}
8483
shouldDisplayContextMenu={false}
85-
shouldShowDraftMessage={false}
8684
shouldShowBorder
8785
personalDetails={personalDetails}
8886
/>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {useMemo} from 'react';
2+
import {getOriginalMessage} from '@libs/ReportActionsUtils';
3+
import type {ReportAction} from '@src/types/onyx';
4+
5+
/**
6+
* Stable `ReportAction` projection for the `ReportActionItem` subtree, with `originalMessage` precomputed.
7+
* Per-field `useMemo` deps so the ref holds when consumed fields are unchanged.
8+
* Allow-list — when a descendant reads a new `reportAction.*` field, add it here.
9+
*/
10+
function useStableReportActionForReportActionItem(reportAction: ReportAction): ReportAction {
11+
const reportActionID = reportAction.reportActionID;
12+
const message = reportAction.message;
13+
const pendingAction = reportAction.pendingAction;
14+
const actionName = reportAction.actionName;
15+
const errors = reportAction.errors;
16+
const childCommenterCount = reportAction.childCommenterCount;
17+
const linkMetadata = reportAction.linkMetadata;
18+
const childReportID = reportAction.childReportID;
19+
const childLastVisibleActionCreated = reportAction.childLastVisibleActionCreated;
20+
const error = reportAction.error;
21+
const created = reportAction.created;
22+
const actorAccountID = reportAction.actorAccountID;
23+
const adminAccountID = reportAction.adminAccountID;
24+
const childVisibleActionCount = reportAction.childVisibleActionCount;
25+
const childOldestFourAccountIDs = reportAction.childOldestFourAccountIDs;
26+
const childType = reportAction.childType;
27+
const person = reportAction.person;
28+
const isOptimisticAction = reportAction.isOptimisticAction;
29+
const delegateAccountID = reportAction.delegateAccountID;
30+
const previousMessage = reportAction.previousMessage;
31+
const isAttachmentWithText = reportAction.isAttachmentWithText;
32+
const isOriginalReportDeleted = reportAction.isOriginalReportDeleted;
33+
const childStateNum = reportAction.childStateNum;
34+
const childStatusNum = reportAction.childStatusNum;
35+
const childReportName = reportAction.childReportName;
36+
const childManagerAccountID = reportAction.childManagerAccountID;
37+
const childMoneyRequestCount = reportAction.childMoneyRequestCount;
38+
const childOwnerAccountID = reportAction.childOwnerAccountID;
39+
40+
const originalMessage = useMemo(() => getOriginalMessage(reportAction), [reportAction]);
41+
42+
return useMemo(
43+
() =>
44+
({
45+
reportActionID,
46+
message,
47+
pendingAction,
48+
actionName,
49+
errors,
50+
originalMessage,
51+
childCommenterCount,
52+
linkMetadata,
53+
childReportID,
54+
childLastVisibleActionCreated,
55+
error,
56+
created,
57+
actorAccountID,
58+
adminAccountID,
59+
childVisibleActionCount,
60+
childOldestFourAccountIDs,
61+
childType,
62+
person,
63+
isOptimisticAction,
64+
delegateAccountID,
65+
previousMessage,
66+
isAttachmentWithText,
67+
isOriginalReportDeleted,
68+
childStateNum,
69+
childStatusNum,
70+
childReportName,
71+
childManagerAccountID,
72+
childMoneyRequestCount,
73+
childOwnerAccountID,
74+
}) as ReportAction,
75+
[
76+
reportActionID,
77+
message,
78+
pendingAction,
79+
actionName,
80+
errors,
81+
originalMessage,
82+
childCommenterCount,
83+
linkMetadata,
84+
childReportID,
85+
childLastVisibleActionCreated,
86+
error,
87+
created,
88+
actorAccountID,
89+
adminAccountID,
90+
childVisibleActionCount,
91+
childOldestFourAccountIDs,
92+
childType,
93+
person,
94+
isOptimisticAction,
95+
delegateAccountID,
96+
previousMessage,
97+
isAttachmentWithText,
98+
isOriginalReportDeleted,
99+
childStateNum,
100+
childStatusNum,
101+
childReportName,
102+
childManagerAccountID,
103+
childMoneyRequestCount,
104+
childOwnerAccountID,
105+
],
106+
);
107+
}
108+
109+
export default useStableReportActionForReportActionItem;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {deepEqual} from 'fast-equals';
2+
import {useMemo, useState} from 'react';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import type {Report} from '@src/types/onyx';
5+
6+
/**
7+
* Stable `Report` projection for the `ReportActionItem` subtree.
8+
* Per-field `useMemo` deps so the ref holds across Onyx pushes when consumed fields are unchanged.
9+
*
10+
* Never add — heartbeat-style fields that update on routine activity and defeat the memo:
11+
* - lastReadTime, lastVisibleActionCreated, lastVisibleActionLastModified
12+
* - lastMessageText, lastMessageHtml
13+
* - lastActorAccountID, lastActionType
14+
*
15+
*/
16+
function useStableReportForReportActionItem(report: OnyxEntry<Report>): OnyxEntry<Report> {
17+
const reportID = report?.reportID;
18+
const chatReportID = report?.chatReportID;
19+
const isWaitingOnBankAccount = report?.isWaitingOnBankAccount;
20+
21+
// Stabilize `permissions` by content
22+
const permissionsRaw = report?.permissions;
23+
const [permissions, setPermissions] = useState(permissionsRaw);
24+
if (!deepEqual(permissions, permissionsRaw)) {
25+
setPermissions(permissionsRaw);
26+
}
27+
28+
const policyID = report?.policyID;
29+
const ownerAccountID = report?.ownerAccountID;
30+
const parentReportID = report?.parentReportID;
31+
const parentReportActionID = report?.parentReportActionID;
32+
const type = report?.type;
33+
const chatType = report?.chatType;
34+
const stateNum = report?.stateNum;
35+
const statusNum = report?.statusNum;
36+
const isDeletedParentAction = report?.isDeletedParentAction;
37+
const pendingFields = report?.pendingFields;
38+
const participants = report?.participants;
39+
const errorFields = report?.errorFields;
40+
const reportName = report?.reportName;
41+
const description = report?.description;
42+
// Coerce sentinel `0` to `undefined`. The backend ships `managerID: 0` on chat reports without a manager, and a later push removes the key entirely; treating both as `undefined` keeps the memo dep stable through that reconciliation.
43+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
44+
const managerID = report?.managerID || undefined;
45+
const total = report?.total;
46+
const nonReimbursableTotal = report?.nonReimbursableTotal;
47+
const policyAvatar = report?.policyAvatar;
48+
const fieldList = report?.fieldList;
49+
const iouReportID = report?.iouReportID;
50+
const isCancelledIOU = report?.isCancelledIOU;
51+
const isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat;
52+
const writeCapability = report?.writeCapability;
53+
const currency = report?.currency;
54+
const visibility = report?.visibility;
55+
const avatarUrl = report?.avatarUrl;
56+
const policyName = report?.policyName;
57+
const transactionCount = report?.transactionCount;
58+
const unheldTotal = report?.unheldTotal;
59+
const created = report?.created;
60+
61+
return useMemo(() => {
62+
if (reportID === undefined) {
63+
return undefined;
64+
}
65+
return {
66+
reportID,
67+
chatReportID,
68+
isWaitingOnBankAccount,
69+
permissions,
70+
policyID,
71+
ownerAccountID,
72+
parentReportID,
73+
parentReportActionID,
74+
type,
75+
chatType,
76+
stateNum,
77+
statusNum,
78+
isDeletedParentAction,
79+
pendingFields,
80+
participants,
81+
errorFields,
82+
reportName,
83+
description,
84+
managerID,
85+
total,
86+
nonReimbursableTotal,
87+
policyAvatar,
88+
fieldList,
89+
iouReportID,
90+
isCancelledIOU,
91+
isOwnPolicyExpenseChat,
92+
writeCapability,
93+
currency,
94+
visibility,
95+
avatarUrl,
96+
policyName,
97+
transactionCount,
98+
unheldTotal,
99+
created,
100+
} as Report;
101+
}, [
102+
reportID,
103+
chatReportID,
104+
isWaitingOnBankAccount,
105+
permissions,
106+
policyID,
107+
ownerAccountID,
108+
parentReportID,
109+
parentReportActionID,
110+
type,
111+
chatType,
112+
stateNum,
113+
statusNum,
114+
isDeletedParentAction,
115+
pendingFields,
116+
participants,
117+
errorFields,
118+
reportName,
119+
description,
120+
managerID,
121+
total,
122+
nonReimbursableTotal,
123+
policyAvatar,
124+
fieldList,
125+
iouReportID,
126+
isCancelledIOU,
127+
isOwnPolicyExpenseChat,
128+
writeCapability,
129+
currency,
130+
visibility,
131+
avatarUrl,
132+
policyName,
133+
transactionCount,
134+
unheldTotal,
135+
created,
136+
]);
137+
}
138+
139+
export default useStableReportForReportActionItem;

src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ function DebugReportActionCreatePage({
112112
parentReportAction={undefined}
113113
displayAsGroup={false}
114114
shouldDisplayNewMarker={false}
115-
index={0}
116115
isFirstVisibleReportAction={false}
117116
shouldDisplayContextMenu={false}
118117
personalDetails={personalDetailsList}

src/pages/Debug/ReportAction/DebugReportActionPreview.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ function DebugReportActionPreview({reportAction, reportID}: DebugReportActionPre
2727
parentReportAction={undefined}
2828
displayAsGroup={false}
2929
shouldDisplayNewMarker={false}
30-
index={0}
3130
isFirstVisibleReportAction={false}
3231
shouldDisplayContextMenu={false}
3332
personalDetails={personalDetails}

src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
77
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
88
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
99
import {getOriginalReportID} from '@libs/ReportUtils';
10+
import ReportActionIndexContext from '@pages/inbox/report/ReportActionIndexContext';
1011
import ReportActionItem from '@pages/inbox/report/ReportActionItem';
1112
import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext';
1213
import CONST from '@src/CONST';
@@ -58,19 +59,20 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
5859
<View style={styles.pb2}>
5960
<ReportActionItemStateContext.Provider value={stateValue}>
6061
<ReportActionItemActionsContext.Provider value={actionsValue}>
61-
<ReportActionItem
62-
action={action}
63-
report={report}
64-
parentReportAction={getReportAction(report?.parentReportID, report?.parentReportActionID)}
65-
index={index}
66-
displayAsGroup={false}
67-
shouldDisplayNewMarker={false}
68-
isFirstVisibleReportAction={false}
69-
shouldDisplayContextMenu={false}
70-
personalDetails={personalDetails}
71-
draftMessage={matchingDraftMessage}
72-
linkedTransactionRouteError={linkedTransactionRouteError}
73-
/>
62+
<ReportActionIndexContext.Provider value={index}>
63+
<ReportActionItem
64+
action={action}
65+
report={report}
66+
parentReportAction={getReportAction(report?.parentReportID, report?.parentReportActionID)}
67+
displayAsGroup={false}
68+
shouldDisplayNewMarker={false}
69+
isFirstVisibleReportAction={false}
70+
shouldDisplayContextMenu={false}
71+
personalDetails={personalDetails}
72+
draftMessage={matchingDraftMessage}
73+
linkedTransactionRouteError={linkedTransactionRouteError}
74+
/>
75+
</ReportActionIndexContext.Provider>
7476
</ReportActionItemActionsContext.Provider>
7577
</ReportActionItemStateContext.Provider>
7678
</View>

0 commit comments

Comments
 (0)