From 01354aea2ed705dc0c7a187863f99ee6d3d45a12 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 24 Mar 2026 12:56:49 +0100 Subject: [PATCH 1/3] perf: remove deepEqual and phantom props from LHN rendering --- .../LHNOptionsList/LHNOptionsList.tsx | 32 +------- .../LHNOptionsList/OptionRowLHNData.tsx | 79 +++---------------- src/components/LHNOptionsList/types.ts | 42 +--------- 3 files changed, 16 insertions(+), 137 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index dafe2c1a26da..39df8dc80a82 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -24,8 +24,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; -import {getIOUReportIDOfLastAction} from '@libs/OptionsListUtils'; -import {getLastVisibleActionIncludingTransactionThread, getOriginalMessage, isActionableTrackExpense, isInviteOrRemovedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {getLastVisibleActionIncludingTransactionThread, getOriginalMessage, isActionableTrackExpense, isInviteOrRemovedAction} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -56,8 +55,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING); const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY); @@ -66,7 +63,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const theme = useTheme(); const styles = useThemeStyles(); - const {translate, preferredLocale, localeCompare} = useLocalize(); + const {translate, localeCompare} = useLocalize(); const isReportsSplitNavigatorLast = useRootNavigationState((state) => state?.routes?.at(-1)?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); const shouldShowEmptyLHN = data.length === 0; const estimatedItemSize = optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight; @@ -166,7 +163,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const itemReportAttributes = reportAttributes?.[reportID]; const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${item.parentReportID}`]; const itemReportNameValuePairs = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; - const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; const itemOneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemReportAttributes?.oneTransactionThreadReportID}`]; const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${item?.parentReportID}`]; const itemParentReportAction = item?.parentReportActionID ? itemParentReportActions?.[item?.parentReportActionID] : undefined; @@ -181,10 +177,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]; const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`]; - const transactionID = isMoneyRequestAction(itemParentReportAction) - ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) - : CONST.DEFAULT_NUMBER_ID; - const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = !!draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] && !draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]?.match(CONST.REGEX.EMPTY_COMMENT); @@ -200,12 +192,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio itemOneTransactionThreadReport?.reportID, ); - const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, itemReportNameValuePairs?.private_isArchived, visibleReportActionsData, lastAction); - const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; - - const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; - const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`]; - // Only override lastMessageTextFromReport when a track expense whisper's transaction has been deleted, to prevent showing stale text. let lastMessageTextFromReport: string | undefined; if (isActionableTrackExpense(lastAction)) { @@ -230,24 +216,17 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio reportAttributesDerived={reportAttributes} oneTransactionThreadReport={itemOneTransactionThreadReport} reportNameValuePairs={itemReportNameValuePairs} - reportActions={itemReportActions} parentReportAction={itemParentReportAction} - iouReportReportActions={itemIouReportReportActions} policy={itemPolicy} invoiceReceiverPolicy={itemInvoiceReceiverPolicy} personalDetails={personalDetails ?? {}} - transaction={itemTransaction} - lastReportActionTransaction={lastReportActionTransaction} viewMode={optionMode} isOptionFocused={!shouldDisableFocusOptions} lastMessageTextFromReport={lastMessageTextFromReport} onSelectRow={onSelectRow} - preferredLocale={preferredLocale} hasDraftComment={hasDraftComment} - transactionViolations={transactionViolations} onLayout={onLayoutItem} shouldShowRBRorGBRTooltip={shouldShowRBRorGBRTooltip} - activePolicyID={activePolicyID} onboardingPurpose={introSelected?.choice} onboarding={onboarding} isFullscreenVisible={isFullscreenVisible} @@ -277,10 +256,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio optionMode, shouldDisableFocusOptions, onSelectRow, - preferredLocale, - transactionViolations, onLayoutItem, - activePolicyID, introSelected?.choice, onboarding, isReportsSplitNavigatorLast, @@ -298,13 +274,11 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio reports, reportAttributes, reportNameValuePairs, - transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, - preferredLocale, transactions, isOffline, isScreenFocused, @@ -316,13 +290,11 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio reports, reportAttributes, reportNameValuePairs, - transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, - preferredLocale, transactions, isOffline, isScreenFocused, diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 4ee59ea96247..32b241701084 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,16 +1,13 @@ -import {deepEqual} from 'fast-equals'; -import React, {useMemo, useRef} from 'react'; +import React, {useMemo} from 'react'; import useReportPreviewSenderID from '@components/ReportActionAvatars/useReportPreviewSenderID'; import {useCurrentReportIDState} from '@hooks/useCurrentReportID'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useOnyx from '@hooks/useOnyx'; -import usePrevious from '@hooks/usePrevious'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import {getMovedReportID} from '@src/libs/ModifiedExpenseMessage'; -import type {OptionData} from '@src/libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import OptionRowLHN from './OptionRowLHN'; @@ -29,16 +26,10 @@ function OptionRowLHNData({ reportAttributesDerived, oneTransactionThreadReport, reportNameValuePairs, - reportActions, personalDetails = {}, - preferredLocale = CONST.LOCALES.DEFAULT, policy, invoiceReceiverPolicy, parentReportAction, - iouReportReportActions, - transaction, - lastReportActionTransaction, - transactionViolations, lastMessageTextFromReport, localeCompare, translate, @@ -51,16 +42,12 @@ function OptionRowLHNData({ const reportID = propsToForward.reportID; const {currentReportID: currentReportIDValue} = useCurrentReportIDState(); const isReportFocused = isOptionFocused && currentReportIDValue === reportID; - const optionItemRef = useRef(undefined); const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`); const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const {login} = useCurrentUserPersonalDetails(); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fullReport?.policyID}`); - // Check the report errors equality to avoid re-rendering when there are no changes - const prevReportErrors = usePrevious(reportAttributes?.reportErrors); - const areReportErrorsEqual = useMemo(() => deepEqual(prevReportErrors, reportAttributes?.reportErrors), [prevReportErrors, reportAttributes?.reportErrors]); const card = useGetExpensifyCardFromReportAction({reportAction: lastAction, policyID: fullReport?.policyID}); @@ -72,76 +59,34 @@ function OptionRowLHNData({ chatReport: chatReportForIOU, }); - const optionItem = useMemo(() => { - // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData({ - report: fullReport, - reportAttributes, - oneTransactionThreadReport, - reportNameValuePairs, - personalDetails, - policy, - parentReportAction, - conciergeReportID, - lastMessageTextFromReport, - invoiceReceiverPolicy, - card, - lastAction, - translate, - localeCompare, - isReportArchived, - lastActionReport, - movedFromReport, - movedToReport, - currentUserAccountID, - reportAttributesDerived, - policyTags, - currentUserLogin: login ?? '', - }); - if (deepEqual(item, optionItemRef.current)) { - return optionItemRef.current; - } - - optionItemRef.current = item; - - return item; - // Listen parentReportAction to update title of thread report when parentReportAction changed - // Listen to transaction to update title of transaction report when transaction changed - // Listen to lastAction to update when action is deleted or gets pendingAction - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - fullReport, - reportAttributes?.brickRoadStatus, - reportAttributes?.actionBadge, - reportAttributes?.actionTargetReportActionID, - reportAttributes?.reportName, - areReportErrorsEqual, + // React Compiler auto-memoizes each expression in OptionRowLHN independently, + // so there is no need to stabilize the optionItem reference with deepEqual. + // When getOptionData returns a fresh object with the same content, the Compiler + // ensures that only expressions whose inputs actually changed recompute. + const optionItem = SidebarUtils.getOptionData({ + report: fullReport, + reportAttributes, oneTransactionThreadReport, reportNameValuePairs, - lastReportActionTransaction, - reportActions, personalDetails, - preferredLocale, policy, parentReportAction, conciergeReportID, - iouReportReportActions, - transaction, - invoiceReceiverPolicy, lastMessageTextFromReport, + invoiceReceiverPolicy, card, lastAction, - lastActionReport, translate, localeCompare, isReportArchived, + lastActionReport, movedFromReport, movedToReport, currentUserAccountID, reportAttributesDerived, policyTags, - login, - ]); + currentUserLogin: login ?? '', + }); // For single-sender IOUs, trim to the sender's avatar to match the header. // The header uses reportPreviewSenderID as accountID for its primary avatar, diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 7172ffc25da4..f2eb64cefe86 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,23 +1,11 @@ import type {RefObject} from 'react'; import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; -import type { - Locale, - Onboarding, - OnboardingPurpose, - PersonalDetailsList, - Policy, - Report, - ReportAction, - ReportActions, - ReportNameValuePairs, - Transaction, - TransactionViolation, -} from '@src/types/onyx'; +import type {Onboarding, OnboardingPurpose, PersonalDetailsList, Policy, Report, ReportAction, ReportNameValuePairs} from '@src/types/onyx'; import type {ReportAttributes, ReportAttributesDerivedValue} from '@src/types/onyx/DerivedValues'; type OptionMode = ValueOf; @@ -54,12 +42,6 @@ type OptionRowLHNDataProps = { /** List of users' personal details */ personalDetails?: PersonalDetailsList; - /** The preferred language for the app */ - preferredLocale?: OnyxEntry; - - /** The active policy ID */ - activePolicyID?: string; - /** The onboarding purpose */ onboardingPurpose?: OnboardingPurpose; @@ -90,32 +72,12 @@ type OptionRowLHNDataProps = { /** The action from the parent report */ parentReportAction?: OnyxEntry; - /** The transaction from the parent report action */ - transaction: OnyxEntry; - - /** The transaction linked to the report's last action */ - lastReportActionTransaction?: OnyxEntry; - /** Whether a report contains a draft */ hasDraftComment: boolean; /** The reportID of the report */ reportID: string; - /** Array of report actions for this report */ - reportActions: OnyxEntry; - - /** - * Array of report actions for the IOU report related to the last action of this report. - * If the last action is a report action preview, the last message of the report depends on - * the report actions of the IOU report linked to the report action preview. - * Changes in the IOU report report actions will affect the last message of this report. - */ - iouReportReportActions: OnyxEntry; - - /** List of transaction violation */ - transactionViolations: OnyxCollection; - /** Toggle between compact and default view */ viewMode?: OptionMode; From afb406a12510e41abb4727c3a990dd864fca6dda Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Thu, 26 Mar 2026 11:18:10 +0100 Subject: [PATCH 2/3] fix: remove duplicate reportAttributes dependency --- src/components/LHNOptionsList/LHNOptionsList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 036a5ff9e5eb..c10fb9e875b0 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -290,7 +290,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio reportActions, reportAttributes, reports, - reportAttributes, reportNameValuePairs, policy, personalDetails, From 5e92ccdb164ee979d182ec2c597888a814033174 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Thu, 26 Mar 2026 11:37:13 +0100 Subject: [PATCH 3/3] fix: remove duplicate reportAttributes props in types --- src/components/LHNOptionsList/types.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 6c1262622b34..f2eb64cefe86 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -121,12 +121,6 @@ type OptionRowLHNDataProps = { /** The current user's account ID */ currentUserAccountID: number; - - /** The report attributes for the report */ - reportAttributes: OnyxEntry; - - /** The derived report attributes for all reports */ - reportAttributesDerived?: ReportAttributesDerivedValue['reports']; }; type OptionRowLHNProps = {