From 64f26aa0a716166e519f128dfa8b3370bcce01fe Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 18 Sep 2025 13:02:33 +0200 Subject: [PATCH 01/12] remove conditional rendering of participants selector --- Mobile-Expensify | 2 +- .../step/IOURequestStepParticipants.tsx | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index e38a301b0e22..f82ce2910e0b 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit e38a301b0e226598189b410e50d856b9bf49da65 +Subproject commit f82ce2910e0b4d878364352b4547ea6ae73807bf diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index dd89cdfe9c0a..4dfeef2cae49 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -353,17 +353,15 @@ function IOURequestStepParticipants({ message={translate('quickAction.noLongerHaveReportAccess')} /> )} - {transactions.length > 0 && ( - - )} + ); } From 9d98a434899729daa26651099e0b2c2f426ec1f5 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 18 Sep 2025 15:56:12 +0200 Subject: [PATCH 02/12] remove navigation side effect --- .../step/IOURequestStepConfirmation.tsx | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 6963fbd3c9a8..757d683244fe 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -74,7 +74,6 @@ import { setMoneyRequestReimbursable, splitBill, splitBillAndOpenReport, - startMoneyRequest, startSplitBill, submitPerDiemExpense as submitPerDiemExpenseIOUActions, trackExpense as trackExpenseIOUActions, @@ -298,35 +297,6 @@ function IOURequestStepConfirmation({ }); }, [transactionIDs, policy, isPolicyExpenseChat]); - useEffect(() => { - // Exit early if the transaction is still loading - if (isLoadingTransaction) { - return; - } - - // Check if the transaction belongs to the current report - const isCurrentReportID = transaction?.isFromGlobalCreate - ? transaction?.participants?.at(0)?.reportID === reportID || (!transaction?.participants?.at(0)?.reportID && transaction?.reportID === reportID) - : transaction?.reportID === reportID; - - // Exit if the transaction already exists and is associated with the current report - if ( - transaction?.transactionID && - (!transaction?.isFromGlobalCreate || !isEmptyObject(transaction?.participants)) && - (isCurrentReportID || isMovingTransactionFromTrackExpense || iouType === CONST.IOU.TYPE.INVOICE) - ) { - return; - } - - startMoneyRequest( - CONST.IOU.TYPE.CREATE, - // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - generateReportID(), - ); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again - }, [isLoadingTransaction, isMovingTransactionFromTrackExpense]); - useEffect(() => { transactions.forEach((item) => { if (!item.category) { From 1b3c4704306c11e9d55ee0c762285a7471f562d8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 19 Sep 2025 08:43:46 +0200 Subject: [PATCH 03/12] set initial transaction when creating expense --- .../step/IOURequestStepParticipants.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 4dfeef2cae49..6b511488bb5f 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -207,9 +207,17 @@ function IOURequestStepParticipants({ const firstParticipantReportID = val.at(0)?.reportID; const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; - transactions.forEach((transaction) => { - setMoneyRequestParticipants(transaction.transactionID, val); - }); + + // Use transactions array if available, otherwise use initialTransactionID directly + // This handles the case where initialTransaction hasn't loaded yet but we still need to set participants + if (transactions.length > 0) { + transactions.forEach((transaction) => { + setMoneyRequestParticipants(transaction.transactionID, val); + }); + } else { + // Fallback to using initialTransactionID directly when transaction object isn't loaded yet + setMoneyRequestParticipants(initialTransactionID, val); + } const isPolicyExpenseChat = !!firstParticipant?.isPolicyExpenseChat; const policy = isPolicyExpenseChat && firstParticipant?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${firstParticipant.policyID}`] : undefined; @@ -218,9 +226,15 @@ function IOURequestStepParticipants({ // If not moving the transaction from track expense, select the default rate automatically. // Otherwise, keep the original p2p rate and let the user manually change it to the one they want from the workspace. const rateID = DistanceRequestUtils.getCustomUnitRateID({reportID: firstParticipantReportID, isPolicyExpenseChat, policy, lastSelectedDistanceRates}); - transactions.forEach((transaction) => { - setCustomUnitRateID(transaction.transactionID, rateID); - }); + + if (transactions.length > 0) { + transactions.forEach((transaction) => { + setCustomUnitRateID(transaction.transactionID, rateID); + }); + } else { + // Fallback to using initialTransactionID directly + setCustomUnitRateID(initialTransactionID, rateID); + } } if (isMovingTransactionFromTrackExpense && isPolicyExpenseChat && policy?.id !== activePolicy?.id) { @@ -246,7 +260,7 @@ function IOURequestStepParticipants({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing selectedReportID.current = firstParticipantReportID || generateReportID(); }, - [iouType, transactions, activePolicy, allPolicies, isMovingTransactionFromTrackExpense, reportID, trackExpense, lastSelectedDistanceRates], + [iouType, transactions, activePolicy, allPolicies, isMovingTransactionFromTrackExpense, reportID, trackExpense, lastSelectedDistanceRates, initialTransactionID], ); const goToNextStep = useCallback(() => { From e262e8f1779d416cbd6e4b36540b7b32ae4ada25 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 19 Sep 2025 08:54:43 +0200 Subject: [PATCH 04/12] Revert "remove navigation side effect" This reverts commit 4a14ed7bf80415ba0d42020d96832804d86ba175. --- .../step/IOURequestStepConfirmation.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 757d683244fe..6963fbd3c9a8 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -74,6 +74,7 @@ import { setMoneyRequestReimbursable, splitBill, splitBillAndOpenReport, + startMoneyRequest, startSplitBill, submitPerDiemExpense as submitPerDiemExpenseIOUActions, trackExpense as trackExpenseIOUActions, @@ -297,6 +298,35 @@ function IOURequestStepConfirmation({ }); }, [transactionIDs, policy, isPolicyExpenseChat]); + useEffect(() => { + // Exit early if the transaction is still loading + if (isLoadingTransaction) { + return; + } + + // Check if the transaction belongs to the current report + const isCurrentReportID = transaction?.isFromGlobalCreate + ? transaction?.participants?.at(0)?.reportID === reportID || (!transaction?.participants?.at(0)?.reportID && transaction?.reportID === reportID) + : transaction?.reportID === reportID; + + // Exit if the transaction already exists and is associated with the current report + if ( + transaction?.transactionID && + (!transaction?.isFromGlobalCreate || !isEmptyObject(transaction?.participants)) && + (isCurrentReportID || isMovingTransactionFromTrackExpense || iouType === CONST.IOU.TYPE.INVOICE) + ) { + return; + } + + startMoneyRequest( + CONST.IOU.TYPE.CREATE, + // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + generateReportID(), + ); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again + }, [isLoadingTransaction, isMovingTransactionFromTrackExpense]); + useEffect(() => { transactions.forEach((item) => { if (!item.category) { From 121c6245c3ddefa6cc0faad37f868d913b7e8a13 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 19 Sep 2025 10:55:07 +0200 Subject: [PATCH 05/12] stabilize props --- .../iou/request/step/IOURequestStepParticipants.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 6b511488bb5f..6d6709b8a434 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -39,6 +39,7 @@ import type SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type Transaction from '@src/types/onyx/Transaction'; +import getEmptyArray from '@src/types/utils/getEmptyArray'; import KeyboardUtils from '@src/utils/keyboard'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; @@ -123,6 +124,7 @@ function IOURequestStepParticipants({ const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID; const isMobileSafari = isMobileSafariBrowser(); + const isPerDiem = isPerDiemRequest(initialTransaction); useEffect(() => { Performance.markEnd(CONST.TIMING.OPEN_CREATE_EXPENSE_CONTACT); @@ -207,7 +209,7 @@ function IOURequestStepParticipants({ const firstParticipantReportID = val.at(0)?.reportID; const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; - + // Use transactions array if available, otherwise use initialTransactionID directly // This handles the case where initialTransaction hasn't loaded yet but we still need to set participants if (transactions.length > 0) { @@ -226,7 +228,7 @@ function IOURequestStepParticipants({ // If not moving the transaction from track expense, select the default rate automatically. // Otherwise, keep the original p2p rate and let the user manually change it to the one they want from the workspace. const rateID = DistanceRequestUtils.getCustomUnitRateID({reportID: firstParticipantReportID, isPolicyExpenseChat, policy, lastSelectedDistanceRates}); - + if (transactions.length > 0) { transactions.forEach((transaction) => { setCustomUnitRateID(transaction.transactionID, rateID); @@ -368,12 +370,12 @@ function IOURequestStepParticipants({ /> )} From 56680b1d65ac0ee58cfc4e0c670c60e9e2a1b468 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 30 Sep 2025 12:11:35 +0200 Subject: [PATCH 06/12] fix conflicts --- src/pages/iou/request/step/IOURequestStepParticipants.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 6d6709b8a434..a67185fc7615 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -125,6 +125,7 @@ function IOURequestStepParticipants({ const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID; const isMobileSafari = isMobileSafariBrowser(); const isPerDiem = isPerDiemRequest(initialTransaction); + const isCorporateCard = isCorporateCardTransaction(initialTransaction); useEffect(() => { Performance.markEnd(CONST.TIMING.OPEN_CREATE_EXPENSE_CONTACT); @@ -376,7 +377,7 @@ function IOURequestStepParticipants({ iouType={iouType} action={action} isPerDiemRequest={isPerDiem} - isCorporateCardTransaction={isCorporateCardTransaction(initialTransaction)} + isCorporateCardTransaction={isCorporateCard} /> ); From 2c2c99104e85fa2b192c658747bacd9d365f94bc Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 30 Sep 2025 12:38:49 +0200 Subject: [PATCH 07/12] revert submodule change --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index f82ce2910e0b..e38a301b0e22 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit f82ce2910e0b4d878364352b4547ea6ae73807bf +Subproject commit e38a301b0e226598189b410e50d856b9bf49da65 From de5acbd3acbc53f3ca8c37ba2c58f84fe543eb35 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 30 Sep 2025 14:40:00 +0200 Subject: [PATCH 08/12] fix memo comparison --- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index a19d6d99f5f3..eb5731ab3048 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -636,4 +636,9 @@ function MoneyRequestParticipantsSelector({ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector'; -export default memo(MoneyRequestParticipantsSelector, (prevProps, nextProps) => deepEqual(prevProps.participants, nextProps.participants) && prevProps.iouType === nextProps.iouType); +export default memo(MoneyRequestParticipantsSelector, (prevProps, nextProps) => + deepEqual(prevProps.participants, nextProps.participants) && + prevProps.iouType === nextProps.iouType && + prevProps.onParticipantsAdded === nextProps.onParticipantsAdded && + prevProps.onFinish === nextProps.onFinish +); From 472e954cf0d56f9e48f1958830e6c6fc640ff6ba Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 30 Sep 2025 14:44:41 +0200 Subject: [PATCH 09/12] fix prettier --- .../iou/request/MoneyRequestParticipantsSelector.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index eb5731ab3048..cffa57cd7a1e 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -636,9 +636,11 @@ function MoneyRequestParticipantsSelector({ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector'; -export default memo(MoneyRequestParticipantsSelector, (prevProps, nextProps) => - deepEqual(prevProps.participants, nextProps.participants) && - prevProps.iouType === nextProps.iouType && - prevProps.onParticipantsAdded === nextProps.onParticipantsAdded && - prevProps.onFinish === nextProps.onFinish +export default memo( + MoneyRequestParticipantsSelector, + (prevProps, nextProps) => + deepEqual(prevProps.participants, nextProps.participants) && + prevProps.iouType === nextProps.iouType && + prevProps.onParticipantsAdded === nextProps.onParticipantsAdded && + prevProps.onFinish === nextProps.onFinish, ); From 56b6a4581344e8e569ff7806504a4568da483bec Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 1 Oct 2025 13:02:34 +0200 Subject: [PATCH 10/12] revert submodule change --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index e38a301b0e22..60d2683fb125 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit e38a301b0e226598189b410e50d856b9bf49da65 +Subproject commit 60d2683fb125730b55a44b09ee50de6e3eb1a374 From f4b1fd4ef86a6d8866719bef0ed890490ecfd26a Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 1 Oct 2025 16:29:38 +0200 Subject: [PATCH 11/12] fix react compiler errors --- .../MoneyRequestParticipantsSelector.tsx | 317 +++++++----------- .../step/IOURequestStepParticipants.tsx | 19 +- 2 files changed, 138 insertions(+), 198 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 79c3978bb30c..75c79b40af80 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -3,7 +3,7 @@ import {emailSelector} from '@selectors/Session'; import {deepEqual} from 'fast-equals'; import lodashPick from 'lodash/pick'; import lodashReject from 'lodash/reject'; -import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {memo, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {Ref} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {InteractionManager} from 'react-native'; @@ -129,15 +129,15 @@ function MoneyRequestParticipantsSelector({ const [textInputAutoFocus, setTextInputAutoFocus] = useState(!isNative); const selectionListRef = useRef(null); - const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); + const cleanSearchTerm = debouncedSearchTerm.trim().toLowerCase(); const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - const isPaidGroupPolicy = useMemo(() => isPaidGroupPolicyUtil(policy), [policy]); + const isPaidGroupPolicy = isPaidGroupPolicyUtil(policy); const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true}); const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; - const canShowManagerMcTest = useMemo(() => !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT, [hasBeenAddedToNudgeMigration, action]); + const canShowManagerMcTest = !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT; useEffect(() => { searchInServer(debouncedSearchTerm.trim()); @@ -149,7 +149,7 @@ function MoneyRequestParticipantsSelector({ initializeOptions(); }, [initializeOptions]); - const defaultOptions = useMemo(() => { + const getDefaultOptions = () => { if (!areOptionsInitialized || !didScreenTransitionEnd) { return { userToInvite: null, @@ -196,23 +196,9 @@ function MoneyRequestParticipantsSelector({ ...optionList, ...orderedOptions, }; - }, [ - action, - contacts, - areOptionsInitialized, - betas, - didScreenTransitionEnd, - iouType, - isCategorizeOrShareAction, - options.personalDetails, - options.reports, - participants, - isPerDiemRequest, - canShowManagerMcTest, - isCorporateCardTransaction, - ]); - - const chatOptions = useMemo(() => { + }; + + const getChatOptions = () => { if (!areOptionsInitialized) { return { userToInvite: null, @@ -224,6 +210,7 @@ function MoneyRequestParticipantsSelector({ selfDMChat: null, }; } + const defaultOptions = getDefaultOptions(); const newOptions = filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, { canInviteUser: !isCategorizeOrShareAction && !isPerDiemRequest, @@ -234,36 +221,28 @@ function MoneyRequestParticipantsSelector({ preferRecentExpenseReports: action === CONST.IOU.ACTION.CREATE, }); return newOptions; - }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, participants, isPaidGroupPolicy, isCategorizeOrShareAction, action, isPerDiemRequest, countryCode]); - - const inputHelperText = useMemo( - () => - getHeaderMessage( - (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length + (chatOptions.workspaceChats ?? []).length !== 0 || - !isEmptyObject(chatOptions.selfDMChat), - !!chatOptions?.userToInvite, - debouncedSearchTerm.trim(), - participants.some((participant) => getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), - ), - [ - chatOptions.personalDetails, - chatOptions.recentReports, - chatOptions.selfDMChat, - chatOptions?.userToInvite, - chatOptions.workspaceChats, - cleanSearchTerm, - debouncedSearchTerm, - participants, - ], + }; + + const chatOptions = getChatOptions(); + + const hasPersonalDetails = (chatOptions.personalDetails ?? []).length > 0; + const hasRecentReports = (chatOptions.recentReports ?? []).length > 0; + const hasWorkspaceChats = (chatOptions.workspaceChats ?? []).length > 0; + const hasSelfDMChat = !isEmptyObject(chatOptions.selfDMChat); + const hasResults = hasPersonalDetails || hasRecentReports || hasWorkspaceChats || hasSelfDMChat; + const inputHelperText = getHeaderMessage( + hasResults, + !!chatOptions?.userToInvite, + debouncedSearchTerm.trim(), + participants.some((participant) => getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), ); /** * Returns the sections needed for the OptionsSelector - * @returns {Array} */ - const [sections, header] = useMemo(() => { + const getSectionsAndHeader = () => { const newSections: Section[] = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { - return [newSections, '']; + return [newSections, ''] as const; } const formatResults = formatSectionsFromSearchTerm( @@ -327,102 +306,79 @@ function MoneyRequestParticipantsSelector({ headerMessage = inputHelperText; } - return [newSections, headerMessage]; - }, [ - areOptionsInitialized, - didScreenTransitionEnd, - debouncedSearchTerm, - participants, - chatOptions.recentReports, - chatOptions.personalDetails, - chatOptions.workspaceChats, - chatOptions.selfDMChat, - chatOptions.userToInvite, - personalDetails, - translate, - isPerDiemRequest, - showImportContacts, - reportAttributesDerived, - inputHelperText, - ]); + return [newSections, headerMessage] as const; + }; + const [sections, header] = getSectionsAndHeader(); /** * Adds a single participant to the expense * - * @param {Object} option + * @param option */ - const addSingleParticipant = useCallback( - (option: Participant & Option) => { - const newParticipants: Participant[] = [ - { - ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'isSelfDM', 'text', 'phoneNumber', 'displayName'), - selected: true, - iouType, - }, - ]; - - if (iouType === CONST.IOU.TYPE.INVOICE) { - const policyID = option.item && isInvoiceRoom(option.item) ? option.policyID : getInvoicePrimaryWorkspace(currentUserLogin)?.id; - newParticipants.push({ - policyID, - isSender: true, - selected: false, - iouType, - }); - } + const addSingleParticipant = (option: Participant & Option) => { + const newParticipants: Participant[] = [ + { + ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'isSelfDM', 'text', 'phoneNumber', 'displayName'), + selected: true, + iouType, + }, + ]; + + if (iouType === CONST.IOU.TYPE.INVOICE) { + const policyID = option.item && isInvoiceRoom(option.item) ? option.policyID : getInvoicePrimaryWorkspace(currentUserLogin)?.id; + newParticipants.push({ + policyID, + isSender: true, + selected: false, + iouType, + }); + } - onParticipantsAdded(newParticipants); + onParticipantsAdded(newParticipants); - if (!option.isSelfDM) { - onFinish(); - } - }, - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes - [onFinish, onParticipantsAdded, currentUserLogin], - ); + if (!option.isSelfDM) { + onFinish(); + } + }; /** * Removes a selected option from list if already selected. If not already selected add this option to the list. - * @param {Object} option + * @param option */ - const addParticipantToSelection = useCallback( - (option: Participant) => { - const isOptionSelected = (selectedOption: Participant) => { - if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { - return true; - } - - if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { - return true; - } + const addParticipantToSelection = (option: Participant) => { + const isOptionSelected = (selectedOption: Participant) => { + if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { + return true; + } - return false; - }; - const isOptionInList = participants.some(isOptionSelected); - let newSelectedOptions: Participant[]; - - if (isOptionInList) { - newSelectedOptions = lodashReject(participants, isOptionSelected); - } else { - newSelectedOptions = [ - ...participants, - { - accountID: option.accountID, - login: option.login, - isPolicyExpenseChat: option.isPolicyExpenseChat, - reportID: option.reportID, - selected: true, - searchText: option.searchText, - iouType, - }, - ]; + if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { + return true; } - onParticipantsAdded(newSelectedOptions); - }, - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes - [participants, onParticipantsAdded], - ); + return false; + }; + const isOptionInList = participants.some(isOptionSelected); + let newSelectedOptions: Participant[]; + + if (isOptionInList) { + newSelectedOptions = lodashReject(participants, isOptionSelected); + } else { + newSelectedOptions = [ + ...participants, + { + accountID: option.accountID, + login: option.login, + isPolicyExpenseChat: option.isPolicyExpenseChat, + reportID: option.reportID, + selected: true, + searchText: option.searchText, + iouType, + }, + ]; + } + + onParticipantsAdded(newSelectedOptions); + }; // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -434,26 +390,23 @@ function MoneyRequestParticipantsSelector({ ![CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE].some((option) => option === iouType) && ![CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE].some((option) => option === action); - const handleConfirmSelection = useCallback( - (keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { - const shouldAddSingleParticipant = option && !participants.length; - if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { - return; - } + const handleConfirmSelection = (keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { + const shouldAddSingleParticipant = option && !participants.length; + if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { + return; + } - if (shouldAddSingleParticipant) { - addSingleParticipant(option); - return; - } + if (shouldAddSingleParticipant) { + addSingleParticipant(option); + return; + } - onFinish(CONST.IOU.TYPE.SPLIT); - }, - [shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants], - ); + onFinish(CONST.IOU.TYPE.SPLIT); + }; - const showLoadingPlaceholder = useMemo(() => !areOptionsInitialized || !didScreenTransitionEnd, [areOptionsInitialized, didScreenTransitionEnd]); + const showLoadingPlaceholder = !areOptionsInitialized || !didScreenTransitionEnd; - const optionLength = useMemo(() => { + const getOptionLength = () => { if (!areOptionsInitialized) { return 0; } @@ -462,19 +415,20 @@ function MoneyRequestParticipantsSelector({ length += section.data.length; }); return length; - }, [areOptionsInitialized, sections]); + }; + const optionLength = getOptionLength(); - const shouldShowListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]); + const shouldShowListEmptyContent = optionLength === 0 && !showLoadingPlaceholder; const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent; - const initiateContactImportAndSetState = useCallback(() => { + const initiateContactImportAndSetState = () => { setContactPermissionState(RESULTS.GRANTED); // eslint-disable-next-line deprecation/deprecation InteractionManager.runAfterInteractions(importAndSaveContacts); - }, [importAndSaveContacts, setContactPermissionState]); + }; - const footerContent = useMemo(() => { + const getFooterContent = () => { if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { return; } @@ -517,37 +471,24 @@ function MoneyRequestParticipantsSelector({ )} ); - }, [ - handleConfirmSelection, - participants.length, - isDismissed, - referralContentType, - shouldShowSplitBillErrorMessage, - styles, - translate, - shouldShowReferralBanner, - isCategorizeOrShareAction, - onFinish, - ]); - - const onSelectRow = useCallback( - (option: Participant) => { - if (option.isPolicyExpenseChat && option.policyID && shouldRestrictUserBillableActions(option.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); - return; - } + }; + const footerContent = getFooterContent(); - if (isIOUSplit) { - addParticipantToSelection(option); - return; - } + const onSelectRow = (option: Participant) => { + if (option.isPolicyExpenseChat && option.policyID && shouldRestrictUserBillableActions(option.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); + return; + } - addSingleParticipant(option); - }, - [isIOUSplit, addParticipantToSelection, addSingleParticipant], - ); + if (isIOUSplit) { + addParticipantToSelection(option); + return; + } - const footerContentAbovePaginationComponent = useMemo(() => { + addSingleParticipant(option); + }; + + const getFooterContentAbovePaginationComponent = () => { if (!showImportContacts) { return null; } @@ -560,9 +501,10 @@ function MoneyRequestParticipantsSelector({ style={styles.mb3} /> ); - }, [showImportContacts, styles.mb3, translate]); + }; + const footerContentAbovePaginationComponent = getFooterContentAbovePaginationComponent(); - const ClickableImportContactTextComponent = useMemo(() => { + const getClickableImportContactTextComponent = () => { if (debouncedSearchTerm.length || isSearchingForReports) { return; } @@ -573,15 +515,14 @@ function MoneyRequestParticipantsSelector({ isInSearch={false} /> ); - }, [debouncedSearchTerm, isSearchingForReports, showImportContacts, translate]); - const EmptySelectionListContentWithPermission = useMemo(() => { - return ( - <> - {ClickableImportContactTextComponent} - - - ); - }, [iouType, ClickableImportContactTextComponent]); + }; + const ClickableImportContactTextComponent = getClickableImportContactTextComponent(); + const EmptySelectionListContentWithPermission = ( + <> + {ClickableImportContactTextComponent} + + + ); useImperativeHandle(ref, () => ({ focus: () => { diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 0359bb6a7f53..e701c65d54d6 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/core'; import {createPoliciesSelector} from '@selectors/Policy'; import {transactionDraftValuesSelector} from '@selectors/TransactionDraft'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; @@ -82,13 +82,12 @@ function IOURequestStepParticipants({ }); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policiesSelector, canBeMissing: true}); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {canBeMissing: true}); - const transactions = useMemo(() => { + const getTransactions = () => { const allTransactions = optimisticTransactions && optimisticTransactions.length > 1 ? optimisticTransactions : [initialTransaction]; return allTransactions.filter((transaction): transaction is Transaction => !!transaction); - }, [initialTransaction, optimisticTransactions]); - // Depend on transactions.length to avoid updating transactionIDs when only the transaction details change - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - const transactionIDs = useMemo(() => transactions?.map((transaction) => transaction.transactionID), [transactions.length]); + }; + const transactions = getTransactions(); + const transactionIDs = transactions?.map((transaction) => transaction.transactionID); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? (participants.at(0)?.reportID ?? reportID) : reportID); @@ -96,7 +95,7 @@ function IOURequestStepParticipants({ const iouRequestType = getRequestType(initialTransaction); const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT; const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseIOUUtils(action); - const headerTitle = useMemo(() => { + const headerTitle = () => { if (action === CONST.IOU.ACTION.CATEGORIZE) { return translate('iou.categorize'); } @@ -113,9 +112,9 @@ function IOURequestStepParticipants({ return translate('workspace.invoices.sendInvoice'); } return translate('iou.chooseRecipient'); - }, [iouType, translate, isSplitRequest, action]); + }; - const selfDMReportID = useMemo(() => findSelfDMReportID(), []); + const selfDMReportID = findSelfDMReportID(); const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); @@ -348,7 +347,7 @@ function IOURequestStepParticipants({ return ( Date: Thu, 2 Oct 2025 09:53:04 +0200 Subject: [PATCH 12/12] Revert "fix react compiler errors" This reverts commit f4b1fd4ef86a6d8866719bef0ed890490ecfd26a. --- .../MoneyRequestParticipantsSelector.tsx | 317 +++++++++++------- .../step/IOURequestStepParticipants.tsx | 19 +- 2 files changed, 198 insertions(+), 138 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 75c79b40af80..79c3978bb30c 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -3,7 +3,7 @@ import {emailSelector} from '@selectors/Session'; import {deepEqual} from 'fast-equals'; import lodashPick from 'lodash/pick'; import lodashReject from 'lodash/reject'; -import React, {memo, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {Ref} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {InteractionManager} from 'react-native'; @@ -129,15 +129,15 @@ function MoneyRequestParticipantsSelector({ const [textInputAutoFocus, setTextInputAutoFocus] = useState(!isNative); const selectionListRef = useRef(null); - const cleanSearchTerm = debouncedSearchTerm.trim().toLowerCase(); + const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - const isPaidGroupPolicy = isPaidGroupPolicyUtil(policy); + const isPaidGroupPolicy = useMemo(() => isPaidGroupPolicyUtil(policy), [policy]); const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true}); const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; - const canShowManagerMcTest = !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT; + const canShowManagerMcTest = useMemo(() => !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT, [hasBeenAddedToNudgeMigration, action]); useEffect(() => { searchInServer(debouncedSearchTerm.trim()); @@ -149,7 +149,7 @@ function MoneyRequestParticipantsSelector({ initializeOptions(); }, [initializeOptions]); - const getDefaultOptions = () => { + const defaultOptions = useMemo(() => { if (!areOptionsInitialized || !didScreenTransitionEnd) { return { userToInvite: null, @@ -196,9 +196,23 @@ function MoneyRequestParticipantsSelector({ ...optionList, ...orderedOptions, }; - }; - - const getChatOptions = () => { + }, [ + action, + contacts, + areOptionsInitialized, + betas, + didScreenTransitionEnd, + iouType, + isCategorizeOrShareAction, + options.personalDetails, + options.reports, + participants, + isPerDiemRequest, + canShowManagerMcTest, + isCorporateCardTransaction, + ]); + + const chatOptions = useMemo(() => { if (!areOptionsInitialized) { return { userToInvite: null, @@ -210,7 +224,6 @@ function MoneyRequestParticipantsSelector({ selfDMChat: null, }; } - const defaultOptions = getDefaultOptions(); const newOptions = filterAndOrderOptions(defaultOptions, debouncedSearchTerm, countryCode, { canInviteUser: !isCategorizeOrShareAction && !isPerDiemRequest, @@ -221,28 +234,36 @@ function MoneyRequestParticipantsSelector({ preferRecentExpenseReports: action === CONST.IOU.ACTION.CREATE, }); return newOptions; - }; - - const chatOptions = getChatOptions(); - - const hasPersonalDetails = (chatOptions.personalDetails ?? []).length > 0; - const hasRecentReports = (chatOptions.recentReports ?? []).length > 0; - const hasWorkspaceChats = (chatOptions.workspaceChats ?? []).length > 0; - const hasSelfDMChat = !isEmptyObject(chatOptions.selfDMChat); - const hasResults = hasPersonalDetails || hasRecentReports || hasWorkspaceChats || hasSelfDMChat; - const inputHelperText = getHeaderMessage( - hasResults, - !!chatOptions?.userToInvite, - debouncedSearchTerm.trim(), - participants.some((participant) => getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), + }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, participants, isPaidGroupPolicy, isCategorizeOrShareAction, action, isPerDiemRequest, countryCode]); + + const inputHelperText = useMemo( + () => + getHeaderMessage( + (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length + (chatOptions.workspaceChats ?? []).length !== 0 || + !isEmptyObject(chatOptions.selfDMChat), + !!chatOptions?.userToInvite, + debouncedSearchTerm.trim(), + participants.some((participant) => getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), + ), + [ + chatOptions.personalDetails, + chatOptions.recentReports, + chatOptions.selfDMChat, + chatOptions?.userToInvite, + chatOptions.workspaceChats, + cleanSearchTerm, + debouncedSearchTerm, + participants, + ], ); /** * Returns the sections needed for the OptionsSelector + * @returns {Array} */ - const getSectionsAndHeader = () => { + const [sections, header] = useMemo(() => { const newSections: Section[] = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { - return [newSections, ''] as const; + return [newSections, '']; } const formatResults = formatSectionsFromSearchTerm( @@ -306,79 +327,102 @@ function MoneyRequestParticipantsSelector({ headerMessage = inputHelperText; } - return [newSections, headerMessage] as const; - }; - const [sections, header] = getSectionsAndHeader(); + return [newSections, headerMessage]; + }, [ + areOptionsInitialized, + didScreenTransitionEnd, + debouncedSearchTerm, + participants, + chatOptions.recentReports, + chatOptions.personalDetails, + chatOptions.workspaceChats, + chatOptions.selfDMChat, + chatOptions.userToInvite, + personalDetails, + translate, + isPerDiemRequest, + showImportContacts, + reportAttributesDerived, + inputHelperText, + ]); /** * Adds a single participant to the expense * - * @param option + * @param {Object} option */ - const addSingleParticipant = (option: Participant & Option) => { - const newParticipants: Participant[] = [ - { - ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'isSelfDM', 'text', 'phoneNumber', 'displayName'), - selected: true, - iouType, - }, - ]; - - if (iouType === CONST.IOU.TYPE.INVOICE) { - const policyID = option.item && isInvoiceRoom(option.item) ? option.policyID : getInvoicePrimaryWorkspace(currentUserLogin)?.id; - newParticipants.push({ - policyID, - isSender: true, - selected: false, - iouType, - }); - } + const addSingleParticipant = useCallback( + (option: Participant & Option) => { + const newParticipants: Participant[] = [ + { + ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'isSelfDM', 'text', 'phoneNumber', 'displayName'), + selected: true, + iouType, + }, + ]; - onParticipantsAdded(newParticipants); + if (iouType === CONST.IOU.TYPE.INVOICE) { + const policyID = option.item && isInvoiceRoom(option.item) ? option.policyID : getInvoicePrimaryWorkspace(currentUserLogin)?.id; + newParticipants.push({ + policyID, + isSender: true, + selected: false, + iouType, + }); + } - if (!option.isSelfDM) { - onFinish(); - } - }; + onParticipantsAdded(newParticipants); + + if (!option.isSelfDM) { + onFinish(); + } + }, + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes + [onFinish, onParticipantsAdded, currentUserLogin], + ); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. - * @param option + * @param {Object} option */ - const addParticipantToSelection = (option: Participant) => { - const isOptionSelected = (selectedOption: Participant) => { - if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { - return true; - } + const addParticipantToSelection = useCallback( + (option: Participant) => { + const isOptionSelected = (selectedOption: Participant) => { + if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { + return true; + } - if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { - return true; - } + if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { + return true; + } - return false; - }; - const isOptionInList = participants.some(isOptionSelected); - let newSelectedOptions: Participant[]; - - if (isOptionInList) { - newSelectedOptions = lodashReject(participants, isOptionSelected); - } else { - newSelectedOptions = [ - ...participants, - { - accountID: option.accountID, - login: option.login, - isPolicyExpenseChat: option.isPolicyExpenseChat, - reportID: option.reportID, - selected: true, - searchText: option.searchText, - iouType, - }, - ]; - } + return false; + }; + const isOptionInList = participants.some(isOptionSelected); + let newSelectedOptions: Participant[]; + + if (isOptionInList) { + newSelectedOptions = lodashReject(participants, isOptionSelected); + } else { + newSelectedOptions = [ + ...participants, + { + accountID: option.accountID, + login: option.login, + isPolicyExpenseChat: option.isPolicyExpenseChat, + reportID: option.reportID, + selected: true, + searchText: option.searchText, + iouType, + }, + ]; + } - onParticipantsAdded(newSelectedOptions); - }; + onParticipantsAdded(newSelectedOptions); + }, + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes + [participants, onParticipantsAdded], + ); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -390,23 +434,26 @@ function MoneyRequestParticipantsSelector({ ![CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE].some((option) => option === iouType) && ![CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE].some((option) => option === action); - const handleConfirmSelection = (keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { - const shouldAddSingleParticipant = option && !participants.length; - if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { - return; - } + const handleConfirmSelection = useCallback( + (keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { + const shouldAddSingleParticipant = option && !participants.length; + if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { + return; + } - if (shouldAddSingleParticipant) { - addSingleParticipant(option); - return; - } + if (shouldAddSingleParticipant) { + addSingleParticipant(option); + return; + } - onFinish(CONST.IOU.TYPE.SPLIT); - }; + onFinish(CONST.IOU.TYPE.SPLIT); + }, + [shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants], + ); - const showLoadingPlaceholder = !areOptionsInitialized || !didScreenTransitionEnd; + const showLoadingPlaceholder = useMemo(() => !areOptionsInitialized || !didScreenTransitionEnd, [areOptionsInitialized, didScreenTransitionEnd]); - const getOptionLength = () => { + const optionLength = useMemo(() => { if (!areOptionsInitialized) { return 0; } @@ -415,20 +462,19 @@ function MoneyRequestParticipantsSelector({ length += section.data.length; }); return length; - }; - const optionLength = getOptionLength(); + }, [areOptionsInitialized, sections]); - const shouldShowListEmptyContent = optionLength === 0 && !showLoadingPlaceholder; + const shouldShowListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]); const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent; - const initiateContactImportAndSetState = () => { + const initiateContactImportAndSetState = useCallback(() => { setContactPermissionState(RESULTS.GRANTED); // eslint-disable-next-line deprecation/deprecation InteractionManager.runAfterInteractions(importAndSaveContacts); - }; + }, [importAndSaveContacts, setContactPermissionState]); - const getFooterContent = () => { + const footerContent = useMemo(() => { if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { return; } @@ -471,24 +517,37 @@ function MoneyRequestParticipantsSelector({ )} ); - }; - const footerContent = getFooterContent(); - - const onSelectRow = (option: Participant) => { - if (option.isPolicyExpenseChat && option.policyID && shouldRestrictUserBillableActions(option.policyID)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); - return; - } + }, [ + handleConfirmSelection, + participants.length, + isDismissed, + referralContentType, + shouldShowSplitBillErrorMessage, + styles, + translate, + shouldShowReferralBanner, + isCategorizeOrShareAction, + onFinish, + ]); + + const onSelectRow = useCallback( + (option: Participant) => { + if (option.isPolicyExpenseChat && option.policyID && shouldRestrictUserBillableActions(option.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); + return; + } - if (isIOUSplit) { - addParticipantToSelection(option); - return; - } + if (isIOUSplit) { + addParticipantToSelection(option); + return; + } - addSingleParticipant(option); - }; + addSingleParticipant(option); + }, + [isIOUSplit, addParticipantToSelection, addSingleParticipant], + ); - const getFooterContentAbovePaginationComponent = () => { + const footerContentAbovePaginationComponent = useMemo(() => { if (!showImportContacts) { return null; } @@ -501,10 +560,9 @@ function MoneyRequestParticipantsSelector({ style={styles.mb3} /> ); - }; - const footerContentAbovePaginationComponent = getFooterContentAbovePaginationComponent(); + }, [showImportContacts, styles.mb3, translate]); - const getClickableImportContactTextComponent = () => { + const ClickableImportContactTextComponent = useMemo(() => { if (debouncedSearchTerm.length || isSearchingForReports) { return; } @@ -515,14 +573,15 @@ function MoneyRequestParticipantsSelector({ isInSearch={false} /> ); - }; - const ClickableImportContactTextComponent = getClickableImportContactTextComponent(); - const EmptySelectionListContentWithPermission = ( - <> - {ClickableImportContactTextComponent} - - - ); + }, [debouncedSearchTerm, isSearchingForReports, showImportContacts, translate]); + const EmptySelectionListContentWithPermission = useMemo(() => { + return ( + <> + {ClickableImportContactTextComponent} + + + ); + }, [iouType, ClickableImportContactTextComponent]); useImperativeHandle(ref, () => ({ focus: () => { diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index e701c65d54d6..0359bb6a7f53 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/core'; import {createPoliciesSelector} from '@selectors/Policy'; import {transactionDraftValuesSelector} from '@selectors/TransactionDraft'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; @@ -82,12 +82,13 @@ function IOURequestStepParticipants({ }); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policiesSelector, canBeMissing: true}); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {canBeMissing: true}); - const getTransactions = () => { + const transactions = useMemo(() => { const allTransactions = optimisticTransactions && optimisticTransactions.length > 1 ? optimisticTransactions : [initialTransaction]; return allTransactions.filter((transaction): transaction is Transaction => !!transaction); - }; - const transactions = getTransactions(); - const transactionIDs = transactions?.map((transaction) => transaction.transactionID); + }, [initialTransaction, optimisticTransactions]); + // Depend on transactions.length to avoid updating transactionIDs when only the transaction details change + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + const transactionIDs = useMemo(() => transactions?.map((transaction) => transaction.transactionID), [transactions.length]); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? (participants.at(0)?.reportID ?? reportID) : reportID); @@ -95,7 +96,7 @@ function IOURequestStepParticipants({ const iouRequestType = getRequestType(initialTransaction); const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT; const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseIOUUtils(action); - const headerTitle = () => { + const headerTitle = useMemo(() => { if (action === CONST.IOU.ACTION.CATEGORIZE) { return translate('iou.categorize'); } @@ -112,9 +113,9 @@ function IOURequestStepParticipants({ return translate('workspace.invoices.sendInvoice'); } return translate('iou.chooseRecipient'); - }; + }, [iouType, translate, isSplitRequest, action]); - const selfDMReportID = findSelfDMReportID(); + const selfDMReportID = useMemo(() => findSelfDMReportID(), []); const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); @@ -347,7 +348,7 @@ function IOURequestStepParticipants({ return (