From 4bf02fb3f6c4ab467643adc1068afd3178e86ede Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 1 Jul 2025 15:36:36 +0200 Subject: [PATCH 01/10] fix animatedScrollHandler recomputations --- src/pages/Search/SearchPageNarrow.tsx | 39 ++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index cf7bb0c8b51f4..37b7ed373b6b0 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -77,25 +77,28 @@ function SearchPageNarrow({queryJSON, headerButtonsOptions, currentSearchResults top: topBarOffset.get(), })); - const scrollHandler = useAnimatedScrollHandler({ - onScroll: (event) => { - runOnJS(triggerScrollEvent)(); - const {contentOffset, layoutMeasurement, contentSize} = event; - if (windowHeight > contentSize.height) { - return; - } - const currentOffset = contentOffset.y; - const isScrollingDown = currentOffset > scrollOffset.get(); - const distanceScrolled = currentOffset - scrollOffset.get(); - - if (isScrollingDown && contentOffset.y > TOO_CLOSE_TO_TOP_DISTANCE) { - topBarOffset.set(clamp(topBarOffset.get() - distanceScrolled, variables.minimalTopBarOffset, StyleUtils.searchHeaderDefaultOffset)); - } else if (!isScrollingDown && distanceScrolled < 0 && contentOffset.y + layoutMeasurement.height < contentSize.height - TOO_CLOSE_TO_BOTTOM_DISTANCE) { - topBarOffset.set(withTiming(StyleUtils.searchHeaderDefaultOffset, {duration: ANIMATION_DURATION_IN_MS})); - } - scrollOffset.set(currentOffset); + const scrollHandler = useAnimatedScrollHandler( + { + onScroll: (event) => { + runOnJS(triggerScrollEvent)(); + const {contentOffset, layoutMeasurement, contentSize} = event; + if (windowHeight > contentSize.height) { + return; + } + const currentOffset = contentOffset.y; + const isScrollingDown = currentOffset > scrollOffset.get(); + const distanceScrolled = currentOffset - scrollOffset.get(); + + if (isScrollingDown && contentOffset.y > TOO_CLOSE_TO_TOP_DISTANCE) { + topBarOffset.set(clamp(topBarOffset.get() - distanceScrolled, variables.minimalTopBarOffset, StyleUtils.searchHeaderDefaultOffset)); + } else if (!isScrollingDown && distanceScrolled < 0 && contentOffset.y + layoutMeasurement.height < contentSize.height - TOO_CLOSE_TO_BOTTOM_DISTANCE) { + topBarOffset.set(withTiming(StyleUtils.searchHeaderDefaultOffset, {duration: ANIMATION_DURATION_IN_MS})); + } + scrollOffset.set(currentOffset); + }, }, - }); + [], + ); const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); From 9384ded071176eaaecdb2a6eddb2c78f2c685124 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 1 Jul 2025 17:29:28 +0200 Subject: [PATCH 02/10] fix setCurrentSearchHash to not cause redundant rerenders --- src/components/Search/SearchContext.tsx | 13 +- src/components/Search/SearchList.tsx | 6 +- src/components/Search/index.tsx | 139 +++++++++--------- .../Search/TransactionListItem.tsx | 16 +- 4 files changed, 93 insertions(+), 81 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 940891a69f8c4..8e03f00bf8e7b 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -41,10 +41,15 @@ function SearchContextProvider({children}: ChildrenProps) { const areTransactionsEmpty = useRef(true); const setCurrentSearchHash = useCallback((searchHash: number) => { - setSearchContextData((prevState) => ({ - ...prevState, - currentSearchHash: searchHash, - })); + setSearchContextData((prevState) => { + if (searchHash === prevState.currentSearchHash) { + return prevState; + } + return { + ...prevState, + currentSearchHash: searchHash, + }; + }); }, []); const setSelectedTransactions: SearchContext['setSelectedTransactions'] = useCallback((selectedTransactions, data = []) => { diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index bf2fdb95a6407..f831c4242e127 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -363,6 +363,10 @@ function SearchList( const selectAllButtonVisible = canSelectMultiple && !SearchTableHeader; const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === flattenedTransactionWithoutPendingDelete.length; + const keyExtractor = useCallback((item: SearchListItem, index: number) => { + return item.keyForList ?? `${index}`; + }, []); + return ( {tableHeaderVisible && ( @@ -399,7 +403,7 @@ function SearchList( item.keyForList ?? `${index}`} + keyExtractor={keyExtractor} onScroll={onScroll} contentContainerStyle={contentContainerStyle} showsVerticalScrollIndicator={false} diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ad47dbc879297..1620d4dc927ce 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -468,83 +468,43 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS [shouldShowLoadingState], ); - if (shouldShowLoadingState) { - return ( - - ); - } - - if (searchResults === undefined) { - Log.alert('[Search] Undefined search type'); - return {null}; - } - - const ListItem = getListItem(type, status, groupBy); - const sortedData = getSortedSections(type, status, data, sortBy, sortOrder, groupBy); - const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || selectionMode?.isEnabled === true); - - const sortedSelectedData = sortedData.map((item) => { - const baseKey = isChat - ? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}` - : `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`; - // Check if the base key matches the newSearchResultKey (TransactionListItemType) - const isBaseKeyMatch = baseKey === newSearchResultKey; - // Check if any transaction within the transactions array (TransactionGroupListItemType) matches the newSearchResultKey - const isAnyTransactionMatch = - !isChat && - (item as TransactionGroupListItemType)?.transactions?.some((transaction) => { - const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; - return transactionKey === newSearchResultKey; - }); - // Determine if either the base key or any transaction key matches - const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch; - - return mapToItemWithAdditionalInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight); - }); + const ListItem = getListItem(type, status, groupBy); + const sortedSelectedData = useMemo( + () => + getSortedSections(type, status, data, sortBy, sortOrder, groupBy).map((item) => { + const baseKey = isChat + ? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}` + : `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`; + // Check if the base key matches the newSearchResultKey (TransactionListItemType) + const isBaseKeyMatch = baseKey === newSearchResultKey; + // Check if any transaction within the transactions array (TransactionGroupListItemType) matches the newSearchResultKey + const isAnyTransactionMatch = + !isChat && + (item as TransactionGroupListItemType)?.transactions?.some((transaction) => { + const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; + return transactionKey === newSearchResultKey; + }); + // Determine if either the base key or any transaction key matches + const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch; + + return mapToItemWithAdditionalInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight); + }), + [type, status, data, sortBy, sortOrder, groupBy, isChat, newSearchResultKey, selectedTransactions, canSelectMultiple], + ); const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; - if (hasErrors) { - return ( - - - - ); - } - - const visibleDataLength = data.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length; - if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults.search.type)) { - return ( - - - - ); - } - - const fetchMoreResults = () => { + const fetchMoreResults = useCallback(() => { if (!searchResults?.search?.hasMoreResults || shouldShowLoadingState || shouldShowLoadingMoreItems) { return; } setOffset(offset + CONST.SEARCH.RESULTS_PAGE_SIZE); - }; + }, [offset, searchResults?.search?.hasMoreResults, shouldShowLoadingMoreItems, shouldShowLoadingState]); - const toggleAllTransactions = () => { + const toggleAllTransactions = useCallback(() => { const areItemsGrouped = !!groupBy; const totalSelected = Object.keys(selectedTransactions).length; @@ -574,7 +534,50 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS ), data, ); - }; + }, [clearSelectedTransactions, data, groupBy, reportActionsArray, selectedTransactions, setSelectedTransactions]); + + const onLayout = useCallback(() => handleSelectionListScroll(sortedSelectedData, searchListRef.current), [handleSelectionListScroll, sortedSelectedData]); + + if (shouldShowLoadingState) { + return ( + + ); + } + + if (searchResults === undefined) { + Log.alert('[Search] Undefined search type'); + return {null}; + } + + if (hasErrors) { + return ( + + + + ); + } + + const visibleDataLength = data.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length; + if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults.search.type)) { + return ( + + + + ); + } const onSortPress = (column: SearchColumnType, order: SortOrder) => { const newQuery = buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order}); @@ -629,7 +632,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS } queryJSON={queryJSON} onViewableItemsChanged={onViewableItemsChanged} - onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)} + onLayout={onLayout} /> ); diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index ccc1d98894c1a..3b2f775830c65 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import type {ValueOf} from 'type-fest'; import {useSearchContext} from '@components/Search/SearchContext'; import BaseListItem from '@components/SelectionList/BaseListItem'; @@ -8,7 +8,7 @@ import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {handleActionButtonPress} from '@libs/actions/Search'; +import {handleActionButtonPress as handleActionButtonPressUtil} from '@libs/actions/Search'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow'; @@ -83,6 +83,10 @@ function TransactionListItem({ [transactionItem?.shouldShowCategory, transactionItem?.shouldShowTag, transactionItem?.shouldShowTax], ); + const handleActionButtonPress = useCallback(() => { + handleActionButtonPressUtil(currentSearchHash, transactionItem, () => onSelectRow(item), shouldUseNarrowLayout && !!canSelectMultiple); + }, [canSelectMultiple, currentSearchHash, item, onSelectRow, shouldUseNarrowLayout, transactionItem]); + return ( ({ {!isLargeScreenWidth && ( { - handleActionButtonPress(currentSearchHash, transactionItem, () => onSelectRow(item), shouldUseNarrowLayout && !!canSelectMultiple); - }} + handleActionButtonPress={handleActionButtonPress} shouldShowUserInfo={!!transactionItem?.from} /> )} { - handleActionButtonPress(currentSearchHash, transactionItem, () => onSelectRow(item), shouldUseNarrowLayout && !!canSelectMultiple); - }} + onButtonPress={handleActionButtonPress} onCheckboxPress={() => onCheckboxPress?.(item)} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} From 3d004eb554d54999c9161e6d1edb9b6c534e8da2 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 3 Jul 2025 13:11:29 +0200 Subject: [PATCH 03/10] fix selectionMode caused rerenders when selection mode is set from false to false --- src/ONYXKEYS.ts | 2 +- src/components/MoneyReportHeader.tsx | 4 +- .../MoneyRequestReportActionsList.tsx | 4 +- .../MoneyRequestReportTransactionList.tsx | 10 ++--- src/components/Search/SearchList.tsx | 44 ++----------------- .../SearchPageHeader/SearchFiltersBar.tsx | 2 +- .../SearchPageHeader/SearchPageHeader.tsx | 2 +- src/components/Search/index.tsx | 18 ++++---- .../SelectionListWithModal/index.tsx | 10 ++--- src/hooks/useMobileSelectionMode.ts | 10 +++-- src/hooks/useSearchBackPress/index.android.ts | 4 +- src/libs/actions/MobileSelectionMode.ts | 4 +- src/pages/RoomMembersPage.tsx | 6 +-- src/pages/Search/SearchPage.tsx | 4 +- src/pages/Search/SearchPageNarrow.tsx | 6 +-- src/pages/workspace/WorkspaceMembersPage.tsx | 12 ++--- .../perDiem/WorkspacePerDiemPage.tsx | 14 +++--- 17 files changed, 62 insertions(+), 94 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bce349022a860..4f48ef0f0e664 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1156,7 +1156,7 @@ type OnyxValuesMapping = { [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.ADD_NEW_COMPANY_CARD]: OnyxTypes.AddNewCompanyCardFeed; [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard; - [ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode; + [ONYXKEYS.MOBILE_SELECTION_MODE]: boolean; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_BILLING_FUND_ID]: number; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 602cefbbb2467..bbe59c8e0fbef 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -880,9 +880,9 @@ function MoneyReportHeader({ [connectedIntegrationName, styles.noWrap, styles.textStrong, translate], ); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); - if (selectionMode?.isEnabled) { + if (selectionMode) { return ( - {shouldUseNarrowLayout && !!selectionMode?.isEnabled && ( + {shouldUseNarrowLayout && !!selectionMode && ( <> null} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 9c79839e48b74..5922dfcf8fea7 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -136,7 +136,7 @@ function MoneyRequestReportTransactionList({ const {isMouseDownOnInput, setMouseUp} = useMouseContext(); const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext(); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); const toggleTransaction = useCallback( (transactionID: string) => { @@ -273,7 +273,7 @@ function MoneyRequestReportTransactionList({ return; } - if (selectionMode?.isEnabled) { + if (selectionMode) { toggleTransaction(transaction.transactionID); return; } @@ -295,7 +295,7 @@ function MoneyRequestReportTransactionList({ if (!isSmallScreenWidth) { return; } - if (selectionMode?.isEnabled) { + if (selectionMode) { toggleTransaction(transaction.transactionID); return; } @@ -312,7 +312,7 @@ function MoneyRequestReportTransactionList({ taxAmountColumnSize={taxAmountColumnSize} shouldShowTooltip shouldUseNarrowLayout={shouldUseNarrowLayout || isMediumScreenWidth} - shouldShowCheckbox={!!selectionMode?.isEnabled || !isSmallScreenWidth} + shouldShowCheckbox={!!selectionMode || !isSmallScreenWidth} onCheckboxPress={toggleTransaction} columns={allReportColumns} scrollToNewTransaction={transaction.transactionID === newTransactions?.at(0)?.transactionID ? scrollToNewTransaction : undefined} @@ -355,7 +355,7 @@ function MoneyRequestReportTransactionList({ title={translate('common.select')} icon={Expensicons.CheckSquare} onPress={() => { - if (!selectionMode?.isEnabled) { + if (!selectionMode) { turnOnMobileSelectionMode(); } toggleTransaction(selectedTransactionID); diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index f831c4242e127..ad8b087ecf665 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -26,7 +26,7 @@ import useOnyxCustomHook from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; -import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {isMobileChrome} from '@libs/Browser'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import variables from '@styles/variables'; @@ -127,12 +127,8 @@ function SearchList( const {isSmallScreenWidth} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); const [longPressedItem, setLongPressedItem] = useState(); - // Check if selection should be on when the modal is opened - const wasSelectionOnRef = useRef(false); - // Keep track of the number of selected items to determine if we should turn off selection mode - const selectionRef = useRef(0); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { canBeMissing: true, @@ -140,38 +136,6 @@ function SearchList( const [allReports] = useOnyxCustomHook(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); - useEffect(() => { - selectionRef.current = selectedItemsLength; - - if (!isSmallScreenWidth) { - if (selectedItemsLength === 0) { - turnOffMobileSelectionMode(); - } - return; - } - if (!isFocused) { - return; - } - if (!wasSelectionOnRef.current && selectedItemsLength > 0) { - wasSelectionOnRef.current = true; - } - if (selectedItemsLength > 0 && !selectionMode?.isEnabled) { - turnOnMobileSelectionMode(); - } else if (selectedItemsLength === 0 && selectionMode?.isEnabled && !wasSelectionOnRef.current) { - turnOffMobileSelectionMode(); - } - }, [selectionMode, isSmallScreenWidth, isFocused, selectedItemsLength]); - - useEffect( - () => () => { - if (selectionRef.current !== 0) { - return; - } - turnOffMobileSelectionMode(); - }, - [], - ); - const handleLongPressRow = useCallback( (item: SearchListItem) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -182,14 +146,14 @@ function SearchList( if ('transactions' in item && item.transactions.length === 0) { return; } - if (selectionMode?.isEnabled) { + if (selectionMode) { onCheckboxPress(item); return; } setLongPressedItem(item); setIsModalVisible(true); }, - [isFocused, isSmallScreenWidth, onCheckboxPress, selectionMode?.isEnabled, shouldPreventLongPressRow], + [isFocused, isSmallScreenWidth, onCheckboxPress, selectionMode, shouldPreventLongPressRow], ); const turnOnSelectionMode = useCallback(() => { diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index b00b0c790dfb3..099af7a1cf97d 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -78,7 +78,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions}: SearchFiltersBarPro const email = session?.email; const hasErrors = Object.keys(currentSearchResults?.errors ?? {}).length > 0 && !isOffline; - const shouldShowSelectedDropdown = headerButtonsOptions.length > 0 && (!shouldUseNarrowLayout || (!!selectionMode && selectionMode.isEnabled)); + const shouldShowSelectedDropdown = headerButtonsOptions.length > 0 && (!shouldUseNarrowLayout || !!selectionMode); const [typeOptions, type] = useMemo(() => { const options = getTypeOptions(allPolicies, email); diff --git a/src/components/Search/SearchPageHeader/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader/SearchPageHeader.tsx index ec763bb28ba12..305b849b4a24e 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeader.tsx @@ -29,7 +29,7 @@ function SearchPageHeader({queryJSON, searchRouterListVisible, hideSearchRouterL const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); - if (shouldUseNarrowLayout && selectionMode?.isEnabled) { + if (shouldUseNarrowLayout && !!selectionMode) { return ( selectedTransactions[key]); - if (selectedKeys.length === 0 && selectionMode?.isEnabled && shouldTurnOffSelectionMode) { + if (selectedKeys.length === 0 && selectionMode && shouldTurnOffSelectionMode) { turnOffMobileSelectionMode(); } // We don't want to run the effect on isFocused change as we only need it to early return when it is false. // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTransactions, selectionMode?.isEnabled, shouldTurnOffSelectionMode]); + }, [selectedTransactions, selectionMode, shouldTurnOffSelectionMode]); useEffect(() => { const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); if (!isSmallScreenWidth) { - if (selectedKeys.length === 0) { + if (selectedKeys.length === 0 && !!selectionMode) { turnOffMobileSelectionMode(); } return; } - if (selectedKeys.length > 0 && !selectionMode?.isEnabled && !isSearchResultsEmpty) { + if (selectedKeys.length > 0 && !selectionMode && !isSearchResultsEmpty) { turnOnMobileSelectionMode(); } // We don't need to run the effect on change of isSearchResultsEmpty. // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]); + }, [isSmallScreenWidth, selectedTransactions, selectionMode]); useEffect(() => { if (isOffline || !isFocused) { @@ -401,7 +401,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const openReport = useCallback( (item: SearchListItem) => { - if (selectionMode?.isEnabled) { + if (selectionMode) { toggleTransaction(item); return; } @@ -448,7 +448,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo})); }, - [hash, selectionMode?.isEnabled, toggleTransaction], + [hash, selectionMode, toggleTransaction], ); const onViewableItemsChanged = useCallback( @@ -470,7 +470,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const isTask = type === CONST.SEARCH.DATA_TYPES.TASK; - const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || selectionMode?.isEnabled === true); + const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || selectionMode === true); const ListItem = getListItem(type, status, groupBy); const sortedSelectedData = useMemo( () => diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx index 2c2a9cd58fdfd..3d81ac6579006 100644 --- a/src/components/SelectionListWithModal/index.tsx +++ b/src/components/SelectionListWithModal/index.tsx @@ -41,7 +41,7 @@ function SelectionListWithModal( const {isSmallScreenWidth} = useResponsiveLayout(); const isFocused = useIsFocused(); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); // Check if selection should be on when the modal is opened const wasSelectionOnRef = useRef(false); // Keep track of the number of selected items to determine if we should turn off selection mode @@ -60,7 +60,7 @@ function SelectionListWithModal( selectionRef.current = selectedItems.length; if (!isSmallScreenWidth) { - if (selectedItems.length === 0) { + if (selectedItems.length === 0 && selectionMode) { turnOffMobileSelectionMode(); } return; @@ -71,9 +71,9 @@ function SelectionListWithModal( if (!wasSelectionOnRef.current && selectedItems.length > 0) { wasSelectionOnRef.current = true; } - if (selectedItems.length > 0 && !selectionMode?.isEnabled) { + if (selectedItems.length > 0 && !selectionMode) { turnOnMobileSelectionMode(); - } else if (selectedItems.length === 0 && selectionMode?.isEnabled && !wasSelectionOnRef.current) { + } else if (selectedItems.length === 0 && selectionMode && !wasSelectionOnRef.current) { turnOffMobileSelectionMode(); } // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps @@ -94,7 +94,7 @@ function SelectionListWithModal( if (!turnOnSelectionModeOnLongPress || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox || (!isFocused && !isScreenFocused)) { return; } - if (isSmallScreenWidth && selectionMode?.isEnabled) { + if (isSmallScreenWidth && selectionMode) { rest?.onCheckboxPress?.(item); return; } diff --git a/src/hooks/useMobileSelectionMode.ts b/src/hooks/useMobileSelectionMode.ts index 8004d3674b3f2..15df21cdfe2d2 100644 --- a/src/hooks/useMobileSelectionMode.ts +++ b/src/hooks/useMobileSelectionMode.ts @@ -1,14 +1,18 @@ -import {useEffect} from 'react'; +import {useEffect, useRef} from 'react'; import {useOnyx} from 'react-native-onyx'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import ONYXKEYS from '@src/ONYXKEYS'; export default function useMobileSelectionMode() { - const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); + const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); + const inintialSelectionModeValueRef = useRef(selectionMode); useEffect(() => { + if (!inintialSelectionModeValueRef.current) { + return; + } turnOffMobileSelectionMode(); }, []); - return {selectionMode}; + return selectionMode; } diff --git a/src/hooks/useSearchBackPress/index.android.ts b/src/hooks/useSearchBackPress/index.android.ts index 65b9a6d3a254c..845b75ac3112f 100644 --- a/src/hooks/useSearchBackPress/index.android.ts +++ b/src/hooks/useSearchBackPress/index.android.ts @@ -11,7 +11,7 @@ const useSearchBackPress: UseSearchBackPress = ({onClearSelection, onNavigationC useFocusEffect( useCallback(() => { const onBackPress = () => { - if (selectionMode?.isEnabled) { + if (selectionMode) { onClearSelection(); turnOffMobileSelectionMode(); return true; @@ -21,7 +21,7 @@ const useSearchBackPress: UseSearchBackPress = ({onClearSelection, onNavigationC }; const backHandler = BackHandler.addEventListener('hardwareBackPress', onBackPress); return () => backHandler.remove(); - }, [selectionMode?.isEnabled, onClearSelection, onNavigationCallBack]), + }, [selectionMode, onClearSelection, onNavigationCallBack]), ); }; diff --git a/src/libs/actions/MobileSelectionMode.ts b/src/libs/actions/MobileSelectionMode.ts index 65a51b8349016..65c6e463461be 100644 --- a/src/libs/actions/MobileSelectionMode.ts +++ b/src/libs/actions/MobileSelectionMode.ts @@ -2,11 +2,11 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; const turnOnMobileSelectionMode = () => { - Onyx.merge(ONYXKEYS.MOBILE_SELECTION_MODE, {isEnabled: true}); + Onyx.merge(ONYXKEYS.MOBILE_SELECTION_MODE, true); }; const turnOffMobileSelectionMode = () => { - Onyx.merge(ONYXKEYS.MOBILE_SELECTION_MODE, {isEnabled: false}); + Onyx.merge(ONYXKEYS.MOBILE_SELECTION_MODE, false); }; export {turnOnMobileSelectionMode, turnOffMobileSelectionMode}; diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index c6512efa51119..a16ca8a6da9c4 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -92,7 +92,7 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); - const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true; + const canSelectMultiple = isSmallScreenWidth ? !!selectionMode : true; /** * Get members for the current room @@ -357,7 +357,7 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { }, [report, backTo], ); - const selectionModeHeader = selectionMode?.isEnabled && isSmallScreenWidth; + const selectionModeHeader = !!selectionMode && isSmallScreenWidth; const customListHeader = useMemo(() => { const header = ( @@ -392,7 +392,7 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { title={selectionModeHeader ? translate('common.selectMultiple') : translate('workspace.common.members')} subtitle={StringUtils.lineBreaksToSpaces(getReportName(report))} onBackButtonPress={() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { setSelectedMembers([]); turnOffMobileSelectionMode(); return; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 80ab6215bf585..ff94003dff746 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -68,7 +68,7 @@ function SearchPage({route}: SearchPageProps) { const {isOffline} = useNetwork(); const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, isExportMode, setExportMode} = useSearchContext(); const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); - const [lastPaymentMethods = {}] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); + const [lastPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); @@ -518,7 +518,7 @@ function SearchPage({route}: SearchPageProps) { shouldShowCancelButton={false} /> - {!!selectionMode && selectionMode?.isEnabled && ( + {!!selectionMode && ( { - if (!selectionMode?.isEnabled) { + if (!selectionMode) { return false; } clearSelectedTransactions(undefined, true); @@ -149,7 +149,7 @@ function SearchPageNarrow({queryJSON, headerButtonsOptions, currentSearchResults shouldShowOfflineIndicator={!!searchResults} > - {!selectionMode?.isEnabled ? ( + {!selectionMode ? ( diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 7584b7c02008d..39eb913971f24 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -139,7 +139,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers ); const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, {canBeMissing: true}); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const currentUserAccountID = Number(session?.accountID); const selectionListRef = useRef(null); @@ -158,7 +158,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers [personalDetails, policy?.employeeList, policy?.owner, policyApproverEmail], ); - const canSelectMultiple = isPolicyAdmin && (shouldUseNarrowLayout ? selectionMode?.isEnabled : true); + const canSelectMultiple = isPolicyAdmin && (shouldUseNarrowLayout ? !!selectionMode : true); const confirmModalPrompt = useMemo(() => { const approverAccountID = selectedEmployees.find((selectedEmployee) => isApprover(policy, selectedEmployee)); @@ -523,12 +523,12 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers ); useEffect(() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { return; } setSelectedEmployees([]); - }, [setSelectedEmployees, selectionMode?.isEnabled]); + }, [setSelectedEmployees, selectionMode]); useSearchBackPress({ onClearSelection: () => setSelectedEmployees([]), @@ -702,7 +702,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers ); }; - const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; + const selectionModeHeader = !!selectionMode && shouldUseNarrowLayout; const headerContent = ( <> @@ -740,7 +740,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers shouldShowOfflineIndicatorInWideScreen shouldShowNonAdmin onBackButtonPress={() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { setSelectedEmployees([]); turnOffMobileSelectionMode(); return; diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index f0af0c688960d..3929a712a4026 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -128,7 +128,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const backTo = route.params?.backTo; const policy = usePolicy(policyID); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: false}); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); const [customUnit, allRatesArray, allSubRates] = useMemo(() => { const customUnits = getPerDiemCustomUnit(policy); @@ -138,7 +138,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { return [customUnits, allRates, allSubRatesMemo]; }, [policy]); - const canSelectMultiple = shouldUseNarrowLayout ? selectionMode?.isEnabled : true; + const canSelectMultiple = shouldUseNarrowLayout ? selectionMode : true; const fetchPerDiem = useCallback(() => { openPolicyPerDiemPage(policyID); @@ -251,7 +251,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { }, [policyID]); const openSubRateDetails = (rate: PolicyOption) => { - if (isSmallScreenWidth && selectionMode?.isEnabled) { + if (isSmallScreenWidth && selectionMode) { toggleSubRate(rate); return; } @@ -354,12 +354,12 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const isLoading = !isOffline && customUnit === undefined; useEffect(() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { return; } setSelectedPerDiem([]); - }, [setSelectedPerDiem, selectionMode?.isEnabled]); + }, [setSelectedPerDiem, selectionMode]); useSearchBackPress({ onClearSelection: () => { @@ -368,7 +368,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { onNavigationCallBack: () => Navigation.goBack(backTo), }); - const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; + const selectionModeHeader = selectionMode && shouldUseNarrowLayout; const headerContent = ( <> @@ -413,7 +413,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { icon={!selectionModeHeader ? Illustrations.PerDiem : undefined} shouldUseHeadlineHeader={!selectionModeHeader} onBackButtonPress={() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { setSelectedPerDiem([]); turnOffMobileSelectionMode(); return; From 7f416c647f2a94420e3fb7c57c6f4250da394d8f Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 3 Jul 2025 13:58:22 +0200 Subject: [PATCH 04/10] stop SearchPage rerenders caused by lastNonEmptySearchResults recomputations --- src/components/Search/index.tsx | 13 ++++++++----- src/libs/SearchUIUtils.ts | 3 +-- src/pages/Search/SearchPage.tsx | 14 ++++++-------- src/pages/Search/SearchPageNarrow.tsx | 11 ++++------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 15d60a53e5c15..b2acc8cb483f8 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -47,6 +47,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ReportAction} from '@src/types/onyx'; import type SearchResults from '@src/types/onyx/SearchResults'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {useSearchContext} from './SearchContext'; import SearchList from './SearchList'; import SearchScopeProvider from './SearchScopeProvider'; @@ -56,8 +57,7 @@ type SearchProps = { queryJSON: SearchQueryJSON; onSearchListScroll?: (event: NativeSyntheticEvent) => void; contentContainerStyle?: StyleProp; - currentSearchResults?: SearchResults; - lastNonEmptySearchResults?: SearchResults; + searchResults?: SearchResults; handleSearch: (value: SearchParams) => void; }; @@ -134,7 +134,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact }; } -function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onSearchListScroll, contentContainerStyle, handleSearch}: SearchProps) { +function Search({queryJSON, searchResults, onSearchListScroll, contentContainerStyle, handleSearch}: SearchProps) { const {isOffline} = useNetwork(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); @@ -182,7 +182,6 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS setCurrentSearchHash(hash); }, [hash, clearSelectedTransactions, setCurrentSearchHash, isFocused]); - const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResults; const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults); useEffect(() => { @@ -240,7 +239,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded // we also need to check that the searchResults matches the type and status of the current search - const isDataLoaded = isSearchDataLoaded(currentSearchResults, lastNonEmptySearchResults, queryJSON); + const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON); const shouldShowLoadingState = !isOffline && (!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0)); const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; @@ -313,6 +312,10 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS }; }); } + if (isEmptyObject(newTransactionList)) { + return; + } + setSelectedTransactions(newTransactionList, data); isRefreshingSelection.current = true; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c342f4c2b37fa..f57e66f4cd7ff 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1631,8 +1631,7 @@ function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: S return !isDataLoaded || dataLength === 0 || !Object.values(CONST.SEARCH.DATA_TYPES).includes(type); } -function isSearchDataLoaded(currentSearchResults: SearchResults | undefined, lastNonEmptySearchResults: SearchResults | undefined, queryJSON: SearchQueryJSON | undefined) { - const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResults; +function isSearchDataLoaded(searchResults: SearchResults | undefined, queryJSON: SearchQueryJSON | undefined) { const {status} = queryJSON ?? {}; const isDataLoaded = diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index ff94003dff746..329311dc4447c 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; @@ -94,7 +94,7 @@ function SearchPage({route}: SearchPageProps) { // eslint-disable-next-line rulesdir/no-default-id-values const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true}); - const [lastNonEmptySearchResults, setLastNonEmptySearchResults] = useState(undefined); + const lastNonEmptySearchResults = useRef(undefined); useEffect(() => { confirmReadyToOpenApp(); @@ -107,7 +107,7 @@ function SearchPage({route}: SearchPageProps) { setLastSearchType(currentSearchResults.search.type); if (currentSearchResults.data) { - setLastNonEmptySearchResults(currentSearchResults); + lastNonEmptySearchResults.current = currentSearchResults; } }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults]); @@ -439,7 +439,7 @@ function SearchPage({route}: SearchPageProps) { const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); const {resetVideoPlayerData} = usePlaybackContext(); - const shouldShowOfflineIndicator = currentSearchResults?.data ?? lastNonEmptySearchResults; + const shouldShowOfflineIndicator = currentSearchResults?.data ?? lastNonEmptySearchResults.current; // TODO: to be refactored in step 3 const PDFThumbnailView = pdfFile ? ( @@ -495,8 +495,7 @@ function SearchPage({route}: SearchPageProps) { diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 83d3dfa30a587..83ee66fb81a12 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -41,11 +41,10 @@ const ANIMATION_DURATION_IN_MS = 300; type SearchPageNarrowProps = { queryJSON?: SearchQueryJSON; headerButtonsOptions: Array>; - currentSearchResults?: SearchResults; - lastNonEmptySearchResults?: SearchResults; + searchResults?: SearchResults; }; -function SearchPageNarrow({queryJSON, headerButtonsOptions, currentSearchResults, lastNonEmptySearchResults}: SearchPageNarrowProps) { +function SearchPageNarrow({queryJSON, headerButtonsOptions, searchResults}: SearchPageNarrowProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); @@ -54,7 +53,6 @@ function SearchPageNarrow({queryJSON, headerButtonsOptions, currentSearchResults const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); const {clearSelectedTransactions} = useSearchContext(); const [searchRouterListVisible, setSearchRouterListVisible] = useState(false); - const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResults; const {isOffline} = useNetwork(); // Controls the visibility of the educational tooltip based on user scrolling. @@ -136,7 +134,7 @@ function SearchPageNarrow({queryJSON, headerButtonsOptions, currentSearchResults ); } - const isDataLoaded = isSearchDataLoaded(currentSearchResults, lastNonEmptySearchResults, queryJSON); + const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON); const shouldShowLoadingState = !isOffline && !isDataLoaded; return ( @@ -207,8 +205,7 @@ function SearchPageNarrow({queryJSON, headerButtonsOptions, currentSearchResults {!searchRouterListVisible && ( Date: Thu, 3 Jul 2025 14:57:11 +0200 Subject: [PATCH 05/10] stop redudndant rerenders caused by SearchContext --- src/components/Search/SearchContext.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 8e03f00bf8e7b..2d4450929455f 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -3,6 +3,7 @@ import {isMoneyRequestReport} from '@libs/ReportUtils'; import {isTransactionCardGroupListItemType, isTransactionListItemType, isTransactionMemberGroupListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {SearchContext, SearchContextData} from './types'; const defaultSearchContextData: SearchContextData = { @@ -112,6 +113,9 @@ function SearchContextProvider({children}: ChildrenProps) { if (searchHashOrClearIDsFlag === searchContextData.currentSearchHash) { return; } + if (searchContextData.selectedReports.length === 0 && isEmptyObject(searchContextData.selectedTransactions) && !searchContextData.shouldTurnOffSelectionMode) { + return; + } setSearchContextData((prevState) => ({ ...prevState, shouldTurnOffSelectionMode, @@ -121,7 +125,13 @@ function SearchContextProvider({children}: ChildrenProps) { setShouldShowExportModeOption(false); setExportMode(false); }, - [searchContextData.currentSearchHash, setSelectedTransactions], + [ + searchContextData.currentSearchHash, + searchContextData.selectedReports.length, + searchContextData.selectedTransactions, + searchContextData.shouldTurnOffSelectionMode, + setSelectedTransactions, + ], ); const removeTransaction: SearchContext['removeTransaction'] = useCallback( From 572bdde8bc4b0717ac66da5de6d67a6111ade1d4 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 3 Jul 2025 16:28:27 +0200 Subject: [PATCH 06/10] delete redundant disable compiler case --- src/components/Search/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index b2acc8cb483f8..f148bdac75346 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -207,13 +207,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } return; } - if (selectedKeys.length > 0 && !selectionMode && !isSearchResultsEmpty) { + if (selectedKeys.length > 0 && !selectionMode) { turnOnMobileSelectionMode(); } - - // We don't need to run the effect on change of isSearchResultsEmpty. - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSmallScreenWidth, selectedTransactions, selectionMode]); useEffect(() => { From 3fbda01ddb37501b19b65d25f7767b3abe7a65f8 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Thu, 3 Jul 2025 17:20:49 +0200 Subject: [PATCH 07/10] fix failing linter --- src/components/Navigation/SearchSidebar.tsx | 2 +- src/hooks/useMobileSelectionMode.ts | 5 +++-- src/hooks/useSearchBackPress/index.android.ts | 2 +- src/pages/ReportParticipantsPage.tsx | 8 ++++---- .../categories/WorkspaceCategoriesPage.tsx | 14 +++++++------- .../distanceRates/PolicyDistanceRatesPage.tsx | 10 +++++----- .../reportFields/ReportFieldsListValuesPage.tsx | 10 +++++----- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 12 ++++++------ src/pages/workspace/tags/WorkspaceViewTagsPage.tsx | 10 +++++----- src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 12 ++++++------ 10 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index 74793bb5515a3..6cea273c6cba7 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -57,7 +57,7 @@ function SearchSidebar({state}: SearchSidebarProps) { } }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults]); - const isDataLoaded = isSearchDataLoaded(currentSearchResults, lastNonEmptySearchResults, queryJSON); + const isDataLoaded = isSearchDataLoaded(currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResults, queryJSON); const shouldShowLoadingState = route?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT ? false : !isOffline && !isDataLoaded; if (shouldUseNarrowLayout) { diff --git a/src/hooks/useMobileSelectionMode.ts b/src/hooks/useMobileSelectionMode.ts index 50d9d107c559c..bf737f8508461 100644 --- a/src/hooks/useMobileSelectionMode.ts +++ b/src/hooks/useMobileSelectionMode.ts @@ -5,10 +5,11 @@ import useOnyx from './useOnyx'; export default function useMobileSelectionMode() { const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); - const inintialSelectionModeValueRef = useRef(selectionMode); + const initialSelectionModeValueRef = useRef(selectionMode); useEffect(() => { - if (!inintialSelectionModeValueRef.current) { + // in case the selection mode is already off at the start, we don't need to turn it off again + if (!initialSelectionModeValueRef.current) { return; } turnOffMobileSelectionMode(); diff --git a/src/hooks/useSearchBackPress/index.android.ts b/src/hooks/useSearchBackPress/index.android.ts index 4e677b0b7620e..dc2d4626bbe3c 100644 --- a/src/hooks/useSearchBackPress/index.android.ts +++ b/src/hooks/useSearchBackPress/index.android.ts @@ -7,7 +7,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type UseSearchBackPress from './types'; const useSearchBackPress: UseSearchBackPress = ({onClearSelection, onNavigationCallBack}) => { - const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); + const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true}); useFocusEffect( useCallback(() => { const onBackPress = () => { diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index e02fa651496de..c45e6c81f301f 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -75,7 +75,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const isReportArchived = useReportIsArchived(report?.reportID); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.reportID}`, {canBeMissing: false}); const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: (attributes) => attributes?.reports, canBeMissing: false}); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const currentUserAccountID = Number(session?.accountID); @@ -83,7 +83,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const isGroupChat = useMemo(() => isGroupChatUtils(report), [report]); const isFocused = useIsFocused(); const {isOffline} = useNetwork(); - const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? selectionMode?.isEnabled : true); + const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? selectionMode : true); const [searchValue, setSearchValue] = useState(''); const {chatParticipants, personalDetailsParticipants} = useMemo( @@ -383,7 +383,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { return translate('common.details'); }, [report, translate, isGroupChat]); - const selectionModeHeader = selectionMode?.isEnabled && isSmallScreenWidth; + const selectionModeHeader = !!selectionMode && isSmallScreenWidth; // eslint-disable-next-line rulesdir/no-negated-variables const memberNotFoundMessage = isGroupChat @@ -401,7 +401,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { { - if (selectionMode?.isEnabled) { + if (selectionMode) { setSelectedMembers([]); turnOffMobileSelectionMode(); return; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 9cbd79292ff96..c709a147a8e01 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -79,7 +79,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const policyId = route.params.policyID; const backTo = route.params?.backTo; const policy = usePolicy(policyId); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyId}`, {canBeMissing: true}); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyId}`, {canBeMissing: true}); @@ -93,7 +93,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const filterCategories = useCallback((category: PolicyCategory | undefined) => !!category && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, []); const [selectedCategories, setSelectedCategories] = useFilteredSelection(policyCategories, filterCategories); - const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true; + const canSelectMultiple = isSmallScreenWidth ? !!selectionMode : true; const fetchCategories = useCallback(() => { openPolicyCategoriesPage(policyId); @@ -203,7 +203,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }; const navigateToCategorySettings = (category: PolicyOption) => { - if (isSmallScreenWidth && selectionMode?.isEnabled) { + if (isSmallScreenWidth && selectionMode) { toggleCategory(category); return; } @@ -400,14 +400,14 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const isLoading = !isOffline && policyCategories === undefined; useEffect(() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { return; } setSelectedCategories([]); - }, [setSelectedCategories, selectionMode?.isEnabled]); + }, [setSelectedCategories, selectionMode]); - const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; + const selectionModeHeader = !!selectionMode && shouldUseNarrowLayout; const headerContent = ( <> @@ -475,7 +475,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { icon={!selectionModeHeader ? Illustrations.FolderOpen : undefined} shouldUseHeadlineHeader={!selectionModeHeader} onBackButtonPress={() => { - if (selectionMode?.isEnabled) { + if (selectionMode) { setSelectedCategories([]); turnOffMobileSelectionMode(); return; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index a1ef02cb5a9cb..77e99e25e5fbd 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -62,9 +62,9 @@ function PolicyDistanceRatesPage({ const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const policy = usePolicy(policyID); - const {selectionMode} = useMobileSelectionMode(); + const selectionMode = useMobileSelectionMode(); - const canSelectMultiple = shouldUseNarrowLayout ? selectionMode?.isEnabled : true; + const canSelectMultiple = shouldUseNarrowLayout ? !!selectionMode : true; const customUnit = getDistanceRateCustomUnit(policy); const customUnitRates: Record = useMemo(() => customUnit?.rates ?? {}, [customUnit]); @@ -325,7 +325,7 @@ function PolicyDistanceRatesPage({ const headerButtons = ( - {(shouldUseNarrowLayout ? !selectionMode?.isEnabled : selectedDistanceRates.length === 0) ? ( + {(shouldUseNarrowLayout ? !selectionMode : selectedDistanceRates.length === 0) ? ( <>