diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js index 00b603cdd7d95..131c57d4c3452 100644 --- a/src/components/Attachments/AttachmentCarousel/index.js +++ b/src/components/Attachments/AttachmentCarousel/index.js @@ -78,7 +78,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl * @param {Object} item * @param {number} index */ - const updatePage = useRef( + const updatePage = useCallback( ({viewableItems}) => { Keyboard.dismiss(); @@ -207,7 +207,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl getItemLayout={getItemLayout} keyExtractor={(item) => item.source} viewabilityConfig={viewabilityConfig} - onViewableItemsChanged={updatePage.current} + onViewableItemsChanged={updatePage} /> )} diff --git a/src/hooks/useInitialValue.ts b/src/hooks/useInitialValue.ts new file mode 100644 index 0000000000000..e42ea044e27a1 --- /dev/null +++ b/src/hooks/useInitialValue.ts @@ -0,0 +1,9 @@ +import {useState} from 'react'; + +// In some places we set initial value on first render, but we don't want to re-run the function +// This hook will memoize the initial value and return that without setter, so it's never changed +// https://github.com/Expensify/App/pull/29643#issuecomment-1765894078 +export default function useInitialValue(initialStateFunc: () => T) { + const [initialValue] = useState(initialStateFunc); + return initialValue; +} diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index e987eff4c7e8e..d342fc225d63a 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -238,7 +238,7 @@ function PopoverReportActionContextMenu(_props, ref) { Report.deleteReportComment(reportIDRef.current, reportActionRef.current); } setIsDeleteCommentConfirmModalVisible(false); - }, [reportActionRef]); + }, []); const hideDeleteModal = () => { callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8111cfa4b6443..108e750516960 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -20,6 +20,7 @@ import reportPropTypes from '../../reportPropTypes'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import getIsReportFullyVisible from '../../../libs/getIsReportFullyVisible'; import {ReactionListContext} from '../ReportScreenContext'; +import useInitialValue from '../../../hooks/useInitialValue'; const propTypes = { /** The report currently being looked at */ @@ -71,9 +72,9 @@ function ReportActionsView(props) { const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); - const hasCachedActions = useRef(_.size(props.reportActions) > 0); + const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); + const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const prevNetworkRef = useRef(props.network); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); @@ -204,7 +205,7 @@ function ReportActionsView(props) { } didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions.current ? CONST.TIMING.WARM : CONST.TIMING.COLD); + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); // Capture the init measurement only once not per each chat switch as the value gets overwritten if (!ReportActionsView.initMeasured) { @@ -226,7 +227,7 @@ function ReportActionsView(props) { report={props.report} onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} - mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} + mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} isLoadingInitialReportActions={props.isLoadingInitialReportActions} diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 8b697cde4880d..479f7824afd59 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -27,6 +27,7 @@ import useNetwork from '../../../hooks/useNetwork'; import useWindowDimensions from '../../../hooks/useWindowDimensions'; import {iouPropTypes, iouDefaultProps} from '../propTypes'; import * as Expensicons from '../../../components/Icon/Expensicons'; +import useInitialValue from '../../../hooks/useInitialValue'; const propTypes = { /** React Navigation route */ @@ -63,10 +64,10 @@ function MoneyRequestConfirmPage(props) { const {isOffline} = useNetwork(); const {windowWidth} = useWindowDimensions(); const prevMoneyRequestId = useRef(props.iou.id); - const iouType = useRef(lodashGet(props.route, 'params.iouType', '')); - const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, props.selectedTab); + const iouType = useInitialValue(() => lodashGet(props.route, 'params.iouType', '')); + const reportID = useInitialValue(() => lodashGet(props.route, 'params.reportID', '')); + const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, props.selectedTab); const isScanRequest = MoneyRequestUtils.isScanRequest(props.selectedTab); - const reportID = useRef(lodashGet(props.route, 'params.reportID', '')); const [receiptFile, setReceiptFile] = useState(); const participants = useMemo( () => @@ -77,7 +78,7 @@ function MoneyRequestConfirmPage(props) { [props.iou.participants, props.personalDetails], ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(props.report)), [props.report]); - const isManualRequestDM = props.selectedTab === CONST.TAB.MANUAL && iouType.current === CONST.IOU.TYPE.REQUEST; + const isManualRequestDM = props.selectedTab === CONST.TAB.MANUAL && iouType === CONST.IOU.TYPE.REQUEST; useEffect(() => { IOU.resetMoneyRequestCategory(); @@ -101,47 +102,47 @@ function MoneyRequestConfirmPage(props) { } FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename).then((file) => { if (!file) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current)); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); } else { const receipt = file; receipt.state = file && isManualRequestDM ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; setReceiptFile(receipt); } }); - }, [props.iou.receiptPath, props.iou.receiptFilename, isManualRequestDM]); + }, [props.iou.receiptPath, props.iou.receiptFilename, isManualRequestDM, iouType, reportID]); useEffect(() => { // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request if (!isDistanceRequest && prevMoneyRequestId.current !== props.iou.id) { // The ID is cleared on completing a request. In that case, we will do nothing. if (props.iou.id) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } return; } // Reset the money request Onyx if the ID in Onyx does not match the ID from params - const moneyRequestId = `${iouType.current}${reportID.current}`; + const moneyRequestId = `${iouType}${reportID}`; const shouldReset = !isDistanceRequest && props.iou.id !== moneyRequestId; if (shouldReset) { IOU.resetMoneyRequestInfo(moneyRequestId); } if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset || ReportUtils.isArchivedRoom(props.report)) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } return () => { prevMoneyRequestId.current = props.iou.id; }; - }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest, props.report]); + }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest, props.report, iouType, reportID]); const navigateBack = () => { let fallback; - if (reportID.current) { - fallback = ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current); + if (reportID) { + fallback = ROUTES.MONEY_REQUEST.getRoute(iouType, reportID); } else { - fallback = ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType.current); + fallback = ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType); } Navigation.goBack(fallback); }; @@ -211,8 +212,8 @@ function MoneyRequestConfirmPage(props) { const trimmedComment = props.iou.comment.trim(); // If we have a receipt let's start the split bill by creating only the action, the transaction, and the group DM if needed - if (iouType.current === CONST.IOU.TYPE.SPLIT && props.iou.receiptPath) { - const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID.current) ? reportID.current : ''; + if (iouType === CONST.IOU.TYPE.SPLIT && props.iou.receiptPath) { + const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID) ? reportID : ''; FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename).then((receipt) => { IOU.startSplitBill( selectedParticipants, @@ -228,7 +229,7 @@ function MoneyRequestConfirmPage(props) { // IOUs created from a group report will have a reportID param in the route. // Since the user is already viewing the report, we don't need to navigate them to the report - if (iouType.current === CONST.IOU.TYPE.SPLIT && CONST.REGEX.NUMBER.test(reportID.current)) { + if (iouType === CONST.IOU.TYPE.SPLIT && CONST.REGEX.NUMBER.test(reportID)) { IOU.splitBill( selectedParticipants, props.currentUserPersonalDetails.login, @@ -237,13 +238,13 @@ function MoneyRequestConfirmPage(props) { trimmedComment, props.iou.currency, props.iou.category, - reportID.current, + reportID, ); return; } // If the request is created from the global create menu, we also navigate the user to the group report - if (iouType.current === CONST.IOU.TYPE.SPLIT) { + if (iouType === CONST.IOU.TYPE.SPLIT) { IOU.splitBillAndOpenReport( selectedParticipants, props.currentUserPersonalDetails.login, @@ -281,6 +282,8 @@ function MoneyRequestConfirmPage(props) { requestMoney, createDistanceRequest, receiptFile, + iouType, + reportID, ], ); @@ -312,11 +315,11 @@ function MoneyRequestConfirmPage(props) { return props.translate('common.distance'); } - if (iouType.current === CONST.IOU.TYPE.SPLIT) { + if (iouType === CONST.IOU.TYPE.SPLIT) { return props.translate('iou.split'); } - if (iouType.current === CONST.IOU.TYPE.SEND) { + if (iouType === CONST.IOU.TYPE.SEND) { return props.translate('common.send'); } @@ -339,13 +342,13 @@ function MoneyRequestConfirmPage(props) { { icon: Expensicons.Receipt, text: props.translate('receipt.addReceipt'), - onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType.current, reportID.current)), + onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType, reportID)), }, ]} /> lodashGet(route, 'params.iouType', '')); + const reportID = useInitialValue(() => lodashGet(route, 'params.reportID', '')); + const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab); + const isSendRequest = iouType === CONST.IOU.TYPE.SEND; const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); const isSplitRequest = iou.id === CONST.IOU.TYPE.SPLIT; const [headerTitle, setHeaderTitle] = useState(); @@ -71,12 +72,13 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { const navigateToConfirmationStep = (moneyRequestType) => { IOU.setMoneyRequestId(moneyRequestType); - Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID.current)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID)); }; - const navigateBack = (forceFallback = false) => { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), forceFallback); - }; + const navigateBack = useCallback((forceFallback = false) => { + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), forceFallback); + // eslint-disable-next-line react-hooks/exhaustive-deps -- no deps as we use only initial values + }, []); useEffect(() => { // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request @@ -89,7 +91,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { } // Reset the money request Onyx if the ID in Onyx does not match the ID from params - const moneyRequestId = `${iouType.current}${reportID.current}`; + const moneyRequestId = `${iouType}${reportID}`; const shouldReset = iou.id !== moneyRequestId; if (shouldReset) { IOU.resetMoneyRequestInfo(moneyRequestId); @@ -101,7 +103,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { return () => { prevMoneyRequestId.current = iou.id; }; - }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest]); + }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, iouType, reportID, navigateBack]); return ( (optionsSelectorRef.current = el)} participants={iou.participants} onAddParticipants={IOU.setMoneyRequestParticipants} - navigateToRequest={() => navigateToConfirmationStep(iouType.current)} + navigateToRequest={() => navigateToConfirmationStep(iouType)} navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.TYPE.SPLIT)} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} - iouType={iouType.current} + iouType={iouType} isDistanceRequest={isDistanceRequest} isScanRequest={isScanRequest} /> diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js index 2c47acd58daa6..6bf0c2e974a44 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.js +++ b/src/pages/settings/Profile/TimezoneSelectPage.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useState, useRef} from 'react'; +import React, {useState} from 'react'; import _ from 'underscore'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import ScreenWrapper from '../../../components/ScreenWrapper'; @@ -11,6 +11,7 @@ import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import SelectionList from '../../../components/SelectionList'; import useLocalize from '../../../hooks/useLocalize'; +import useInitialValue from '../../../hooks/useInitialValue'; const propTypes = { ...withCurrentUserPersonalDetailsPropTypes, @@ -36,7 +37,7 @@ const getUserTimezone = (currentUserPersonalDetails) => lodashGet(currentUserPer function TimezoneSelectPage(props) { const {translate} = useLocalize(); const timezone = getUserTimezone(props.currentUserPersonalDetails); - const allTimezones = useRef( + const allTimezones = useInitialValue(() => _.chain(TIMEZONES) .filter((tz) => !tz.startsWith('Etc/GMT')) .map((text) => ({ @@ -47,7 +48,7 @@ function TimezoneSelectPage(props) { .value(), ); const [timezoneInputText, setTimezoneInputText] = useState(''); - const [timezoneOptions, setTimezoneOptions] = useState(allTimezones.current); + const [timezoneOptions, setTimezoneOptions] = useState(allTimezones); /** * @param {Object} timezone @@ -64,7 +65,7 @@ function TimezoneSelectPage(props) { setTimezoneInputText(searchText); const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) || []; setTimezoneOptions( - _.filter(allTimezones.current, (tz) => + _.filter(allTimezones, (tz) => _.every( searchWords, (word) =>