From 3dbd6cd7218fb9ca44944815ea245371878e30f4 Mon Sep 17 00:00:00 2001 From: Benjamin Limpich Date: Fri, 26 Jun 2026 17:41:50 -0700 Subject: [PATCH] Revert "fix: prevent "Something went wrong" screen flash on failed spend query" --- src/components/Search/index.tsx | 51 ++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ed184479a3fd..f439bfa2cef5 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,4 +1,4 @@ -import {useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; +import {findFocusedRoute, useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; import * as Sentry from '@sentry/react-native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; @@ -67,16 +67,19 @@ import { import {cancelSubmitFollowUpActionSpan, getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {isTransactionPendingDelete, shouldShowAttendees} from '@libs/TransactionUtils'; -import Navigation from '@navigation/Navigation'; +import Navigation, {navigationRef} from '@navigation/Navigation'; import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; import EmptySearchView from '@pages/Search/EmptySearchView'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import {hasCompletedGuidedSetupFlowSelector, hasSeenTourSelector} from '@src/selectors/Onboarding'; import type {SaveSearch} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type SearchResults from '@src/types/onyx/SearchResults'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getEmptyArray from '@src/types/utils/getEmptyArray'; import ExpenseFlatSearchView from './ExpenseFlatSearchView'; import useSearchSnapshot from './hooks/useSearchSnapshot'; @@ -125,6 +128,7 @@ function Search({ const {type, status, sortBy, sortOrder, hash, similarSearchHash, groupBy, view} = queryJSON; const {isOffline} = useNetwork(); + const prevIsOffline = usePrevious(isOffline); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth, isLargeScreenWidth, isInLandscapeMode} = useResponsiveLayout(); const styles = useThemeStyles(); @@ -216,6 +220,7 @@ function Search({ showPendingExpensePlaceholder, shouldDeferHeavySearchWork, setShouldDeferHeavySearchWork, + hasPendingWriteOnMountRef, skipDeferralOnFocusRef, rearmTracking, } = useSearchSnapshot({queryJSON, searchResults, newSearchResultKeys, transactions, reportActions}); @@ -353,7 +358,27 @@ function Search({ const shouldRetrySearchWithTotalsOrGroupedRef = useRef(false); useEffect(() => { - if (offset === 0 || offset === searchResults?.search?.offset || !isFocused || isOffline || searchResults?.search?.isLoading) { + const focusedRoute = findFocusedRoute(navigationRef.getRootState()); + const isMigratedModalDisplayed = focusedRoute?.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR || focusedRoute?.name === SCREENS.MIGRATED_USER_WELCOME_MODAL.DYNAMIC_ROOT; + + const comingBackOnlineWithNoResults = prevIsOffline && !isOffline && isEmptyObject(searchResults?.data); + if (!comingBackOnlineWithNoResults && ((!isFocused && !isMigratedModalDisplayed) || isOffline)) { + return; + } + + // When mounting after the pre-insert fast path, the deferred write hasn't + // been flushed yet. Triggering a search now would race with the CREATE + // API call and return stale results that overwrite the optimistic row. + // Skip this call; the optimistic data from flushDeferredWrite will populate + // the list, and the next user-driven search will refresh from the server. + if (hasPendingWriteOnMountRef.current.hasPendingWriteOnMount && hasDeferredWrite(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH)) { + return; + } + + if (searchResults?.search?.isLoading) { + if (validGroupBy || (shouldCalculateTotals && searchResults?.search?.count === undefined)) { + shouldRetrySearchWithTotalsOrGroupedRef.current = true; + } return; } @@ -361,21 +386,14 @@ function Search({ queryJSON, searchKey: currentSearchKey, offset, - shouldCalculateTotals: false, + shouldCalculateTotals, prevReportsLength: filteredDataLength, - isLoading: false, + isLoading: !!searchResults?.search?.isLoading, }); - }, [currentSearchKey, filteredDataLength, handleSearch, isFocused, isOffline, offset, queryJSON, searchResults?.search?.isLoading, searchResults?.search?.offset]); - useEffect(() => { - if (!searchResults?.search?.isLoading) { - return; - } - - if (validGroupBy || (shouldCalculateTotals && searchResults?.search?.count === undefined)) { - shouldRetrySearchWithTotalsOrGroupedRef.current = true; - } - }, [searchResults?.search?.isLoading, searchResults?.search?.count, shouldCalculateTotals, validGroupBy]); + // We don't need to run the effect on change of isFocused. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [handleSearch, isOffline, offset, queryJSON, currentSearchKey, shouldCalculateTotals, validGroupBy]); useEffect(() => { if (!shouldRetrySearchWithTotalsOrGroupedRef.current || searchResults?.search?.isLoading || (!shouldCalculateTotals && !validGroupBy)) { @@ -768,7 +786,8 @@ function Search({ return; } - // Re-arm pending expense skeleton for subsequent creations while Search stays mounted. + // Re-arm pending expense skeleton for subsequent creations while Search + // stays mounted (the original hasPendingWriteOnMountRef only covers the first). if (hasDeferredWrite(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH) && !showPendingExpensePlaceholder) { wasRearmedRef.current = true; rearmTracking();