From c2d83312e404bbdd9becce05d8d309ab9570e7a1 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Fri, 27 Feb 2026 12:18:55 +0100 Subject: [PATCH] refactor: extract filter sync and receipt scan drop from SearchPage Move useFilterFormValues + updateAdvancedFilters into a renderless SearchFilterSync component and push useReceiptScanDrop + DragAndDrop into SearchPageNarrow/SearchPageWide, removing ~16 Onyx subscriptions from the parent SearchPage. Also tighten the POLICY collection selector in useReceiptScanDrop to return a boolean instead of the full map. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useReceiptScanDrop.tsx | 5 +- src/pages/Search/SearchFilterSync.tsx | 25 ++++ src/pages/Search/SearchPage.tsx | 57 ++------ src/pages/Search/SearchPageNarrow.tsx | 201 ++++++++++++++------------ src/pages/Search/SearchPageWide.tsx | 10 +- 5 files changed, 151 insertions(+), 147 deletions(-) create mode 100644 src/pages/Search/SearchFilterSync.tsx diff --git a/src/hooks/useReceiptScanDrop.tsx b/src/hooks/useReceiptScanDrop.tsx index 77c129d6f5690..0c7bf255eb6df 100644 --- a/src/hooks/useReceiptScanDrop.tsx +++ b/src/hooks/useReceiptScanDrop.tsx @@ -1,4 +1,3 @@ -import {useMemo} from 'react'; import {setTransactionReport} from '@libs/actions/Transaction'; import {navigateToParticipantPage} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -27,7 +26,7 @@ function useReceiptScanDrop() { const isAnonymousUser = useIsAnonymousUser(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const selfDMReport = useSelfDMReport(); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [hasOnlyPersonalPolicies = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: hasOnlyPersonalPoliciesUtil}); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); @@ -39,8 +38,6 @@ function useReceiptScanDrop() { const [newReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${newReportID}`); const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${newReport?.parentReportID}`); - const hasOnlyPersonalPolicies = useMemo(() => hasOnlyPersonalPoliciesUtil(policies), [policies]); - const saveFileAndInitMoneyRequest = (files: FileObject[]) => { const initialTransaction = initMoneyRequest({ isFromGlobalCreate: true, diff --git a/src/pages/Search/SearchFilterSync.tsx b/src/pages/Search/SearchFilterSync.tsx new file mode 100644 index 0000000000000..36b24ffc1769c --- /dev/null +++ b/src/pages/Search/SearchFilterSync.tsx @@ -0,0 +1,25 @@ +import {useEffect} from 'react'; +import type {SearchQueryJSON} from '@components/Search/types'; +import useFilterFormValues from '@hooks/useFilterFormValues'; +import {updateAdvancedFilters} from '@libs/actions/Search'; + +type SearchFilterSyncProps = { + queryJSON?: SearchQueryJSON; +}; + +/** + * Component that does not render anything but owns the filter-form Onyx subscriptions (~6 keys) + * and syncs them to the advanced filters form whenever the query changes. + * Extracted from SearchPage to isolate re-renders caused by these subscriptions. + */ +function SearchFilterSync({queryJSON}: SearchFilterSyncProps) { + const formValues = useFilterFormValues(queryJSON); + + useEffect(() => { + updateAdvancedFilters(formValues, true); + }, [formValues]); + + return null; +} + +export default SearchFilterSync; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index ce9f6d13e8018..03d0e11d25380 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,59 +1,42 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import Animated from 'react-native-reanimated'; -import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; -import DragAndDropProvider from '@components/DragAndDrop/Provider'; -import DropZoneUI from '@components/DropZone/DropZoneUI'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import type {SearchParams} from '@components/Search/types'; import {usePlaybackActionsContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; -import useFilterFormValues from '@hooks/useFilterFormValues'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import usePrevious from '@hooks/usePrevious'; -import useReceiptScanDrop from '@hooks/useReceiptScanDrop'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchInServer} from '@libs/actions/Report'; -import {search, updateAdvancedFilters} from '@libs/actions/Search'; +import {search} from '@libs/actions/Search'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import type {SearchResults} from '@src/types/onyx'; +import SearchFilterSync from './SearchFilterSync'; import SearchPageNarrow from './SearchPageNarrow'; import SearchPageWide from './SearchPageWide'; type SearchPageProps = PlatformStackScreenProps; function SearchPage({route}: SearchPageProps) { - const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); - const theme = useTheme(); const {selectedTransactions, lastSearchType, areAllMatchingItemsSelected, currentSearchKey, currentSearchResults} = useSearchStateContext(); const {clearSelectedTransactions, setLastSearchType} = useSearchActionsContext(); const isMobileSelectionModeEnabled = useMobileSelectionMode(clearSelectedTransactions); const queryJSON = useMemo(() => buildSearchQueryJSON(route.params.q, route.params.rawQuery), [route.params.q, route.params.rawQuery]); const {saveScrollOffset} = useContext(ScrollOffsetContext); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan'] as const); const lastNonEmptySearchResults = useRef(undefined); - const formValues = useFilterFormValues(queryJSON); - - useEffect(() => { - updateAdvancedFilters(formValues, true); - }, [formValues]); - useConfirmReadyToOpenApp(); useEffect(() => { @@ -69,7 +52,6 @@ function SearchPage({route}: SearchPageProps) { const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); - const {initScanRequest, PDFValidationComponent, ErrorModal, isDragDisabled} = useReceiptScanDrop(); const {resetVideoPlayerData} = usePlaybackActionsContext(); const [isSorting, setIsSorting] = useState(false); @@ -160,29 +142,16 @@ function SearchPage({route}: SearchPageProps) { return ( + {shouldUseNarrowLayout ? ( - - {PDFValidationComponent} - - - - - {ErrorModal} - + ) : ( )} diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 0cd54fe4b190a..f04198100c268 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -4,6 +4,9 @@ import {View} from 'react-native'; import Animated, {clamp, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {scheduleOnRN} from 'react-native-worklets'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; +import DragAndDropProvider from '@components/DragAndDrop/Provider'; +import DropZoneUI from '@components/DropZone/DropZoneUI'; import {useFullScreenBlockingViewActions} from '@components/FullScreenBlockingViewContextProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import NavigationTabBar from '@components/Navigation/NavigationTabBar'; @@ -18,11 +21,14 @@ import SearchFiltersBar from '@components/Search/SearchPageHeader/SearchFiltersB import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHeader'; import type {SearchParams, SearchQueryJSON} from '@components/Search/types'; import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useReceiptScanDrop from '@hooks/useReceiptScanDrop'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useScrollEventEmitter from '@hooks/useScrollEventEmitter'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -58,7 +64,10 @@ function SearchPageNarrow({queryJSON, searchResults, isMobileSelectionModeEnable const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); const styles = useThemeStyles(); + const theme = useTheme(); const StyleUtils = useStyleUtils(); + const {initScanRequest, PDFValidationComponent, ErrorModal, isDragDisabled} = useReceiptScanDrop(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan'] as const); const {clearSelectedTransactions} = useSearchActionsContext(); const [searchRouterListVisible, setSearchRouterListVisible] = useState(false); const {isOffline} = useNetwork(); @@ -162,100 +171,114 @@ function SearchPageNarrow({queryJSON, searchResults, isMobileSelectionModeEnable const shouldShowLoadingState = !isOffline && (!isDataLoaded || !!metadata?.isLoading); return ( - } - shouldShowOfflineIndicator={!!searchResults} - > - - {!isMobileSelectionModeEnabled ? ( - - - - - - - - { - setSearchRouterListVisible(false); - }} - onSearchRouterFocus={() => { - topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); - setSearchRouterListVisible(true); - }} - handleSearch={handleSearchAction} - isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} - /> - - - {!searchRouterListVisible && ( - + {PDFValidationComponent} + } + shouldShowOfflineIndicator={!!searchResults} + > + + {!isMobileSelectionModeEnabled ? ( + + + + + + + + { + setSearchRouterListVisible(false); + }} + onSearchRouterFocus={() => { + topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); + setSearchRouterListVisible(true); + }} + handleSearch={handleSearchAction} isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} /> - )} - - + + + {!searchRouterListVisible && ( + + )} + + + - - ) : ( - <> - { - topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); - clearSelectedTransactions(); - turnOffMobileSelectionMode(); - }} - /> - - - )} - {!searchRouterListVisible && ( - - + { + topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); + clearSelectedTransactions(); + turnOffMobileSelectionMode(); + }} + /> + + + )} + {!searchRouterListVisible && ( + + + + )} + {shouldShowFooter && !searchRouterListVisible && ( + - - )} - {shouldShowFooter && !searchRouterListVisible && ( - - )} - - + )} + + + + + + {ErrorModal} + ); } diff --git a/src/pages/Search/SearchPageWide.tsx b/src/pages/Search/SearchPageWide.tsx index f98317865f3fb..160d9dce0e51d 100644 --- a/src/pages/Search/SearchPageWide.tsx +++ b/src/pages/Search/SearchPageWide.tsx @@ -14,6 +14,7 @@ import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHead import type {SearchParams, SearchQueryJSON} from '@components/Search/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useReceiptScanDrop from '@hooks/useReceiptScanDrop'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; @@ -34,10 +35,6 @@ type SearchPageWideProps = { handleSearchAction: (value: SearchParams | string) => void; onSortPressedCallback: () => void; scrollHandler: (event: NativeSyntheticEvent) => void; - initScanRequest: (e: DragEvent) => void; - isDragDisabled: boolean; - PDFValidationComponent: React.ReactNode; - ErrorModal: React.ReactNode; shouldShowFooter: boolean; }; @@ -50,15 +47,12 @@ function SearchPageWide({ handleSearchAction, onSortPressedCallback, scrollHandler, - initScanRequest, - isDragDisabled, - PDFValidationComponent, - ErrorModal, shouldShowFooter, }: SearchPageWideProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); + const {initScanRequest, PDFValidationComponent, ErrorModal, isDragDisabled} = useReceiptScanDrop(); const offlineIndicatorStyle = useMemo(() => { if (shouldShowFooter) {