From cf15950535d7a3ef8e268752467d06dc419865cf Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Wed, 25 Feb 2026 16:30:45 +0100 Subject: [PATCH 1/3] perf: delay render of selection list inside search autocomplete --- .../Search/SearchAutocompleteList.tsx | 6 +++++- .../index.native.ts | 21 +++++++++++++++++++ .../useSearchAutocompleteTransition/index.ts | 9 ++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/components/Search/useSearchAutocompleteTransition/index.native.ts create mode 100644 src/components/Search/useSearchAutocompleteTransition/index.ts diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 9a913a37bc94f..6cee4176edefb 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -55,6 +55,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import {getSubstitutionMapKey} from './SearchRouter/getQueryWithSubstitutions'; import type {SearchFilterKey, UserFriendlyKey} from './types'; +import useSearchAutocompleteTransition from './useSearchAutocompleteTransition'; type AutocompleteItemData = { filterKey: UserFriendlyKey; @@ -915,7 +916,10 @@ function SearchAutocompleteList({ }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]); const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); - const isLoading = !isRecentSearchesDataLoaded || !areOptionsInitialized; + + const shouldRender = useSearchAutocompleteTransition(); + + const isLoading = !isRecentSearchesDataLoaded || !areOptionsInitialized || !shouldRender; if (isLoading) { return ( diff --git a/src/components/Search/useSearchAutocompleteTransition/index.native.ts b/src/components/Search/useSearchAutocompleteTransition/index.native.ts new file mode 100644 index 0000000000000..064227c7f4d7a --- /dev/null +++ b/src/components/Search/useSearchAutocompleteTransition/index.native.ts @@ -0,0 +1,21 @@ +import React, {useDeferredValue, useEffect} from 'react'; +import Navigation from '@libs/Navigation/Navigation'; + +/** + * This hook delays component render by creating a state that only updates when the navigation is ready. + * As we wait for the navigation, it smoothly opens the SearchRouter and allows the selection list to render. + */ +function useSearchAutocompleteTransition() { + const [shouldRender, setShouldRender] = React.useState(false); + const deferredValue = useDeferredValue(shouldRender); + + useEffect(() => { + Navigation.isNavigationReady().then(() => { + setShouldRender(true); + }); + }, []); + + return deferredValue; +} + +export default useSearchAutocompleteTransition; diff --git a/src/components/Search/useSearchAutocompleteTransition/index.ts b/src/components/Search/useSearchAutocompleteTransition/index.ts new file mode 100644 index 0000000000000..d36f526e3340a --- /dev/null +++ b/src/components/Search/useSearchAutocompleteTransition/index.ts @@ -0,0 +1,9 @@ +/** + * The rendering is fast enough for the web, so only a native implementation is required. See index.native.ts. + */ + +function useSearchAutocompleteTransition() { + return true; +} + +export default useSearchAutocompleteTransition; From 0eac54375ee2d135047aab5186286db0076e5912 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Thu, 26 Feb 2026 16:04:08 +0100 Subject: [PATCH 2/3] refactor: convert hook to a wrapper --- .../index.native.tsx | 39 +++++++++++++++++++ .../index.tsx} | 7 +--- .../Search/SearchAutocompleteList.tsx | 20 ++-------- .../Search/SearchRouter/SearchRouter.tsx | 4 +- .../index.native.ts | 21 ---------- 5 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 src/components/Search/DeferredSearchAutocompleteList/index.native.tsx rename src/components/Search/{useSearchAutocompleteTransition/index.ts => DeferredSearchAutocompleteList/index.tsx} (50%) delete mode 100644 src/components/Search/useSearchAutocompleteTransition/index.native.ts diff --git a/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx b/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx new file mode 100644 index 0000000000000..bd18308989aa8 --- /dev/null +++ b/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx @@ -0,0 +1,39 @@ +import React, {useDeferredValue, useEffect} from 'react'; +import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; +import type {SearchAutocompleteListProps} from '@components/Search/SearchAutocompleteList'; +import SearchAutocompleteList from '@components/Search/SearchAutocompleteList'; +import Navigation from '@navigation/Navigation'; +import CONST from '@src/CONST'; + +/** + * This component acts as a wrapper for a SearchAutocompleteList, waiting for the navigation to be ready and deferring it, + * so that the base UI can render before the list is loaded. + * This enables the SearchRouterPage to open smoothly with a placeholder and load the list in the meantime. + */ +function DeferredAutocompleteList(props: SearchAutocompleteListProps) { + const [shouldRender, setShouldRender] = React.useState(false); + const deferredShouldRender = useDeferredValue(shouldRender); + + useEffect(() => { + Navigation.isNavigationReady().then(() => { + setShouldRender(true); + }); + }, []); + + if (!deferredShouldRender) { + return ( + + ); + } + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +DeferredAutocompleteList.displayName = 'DeferredSearchAutocompleteList'; + +export default DeferredAutocompleteList; diff --git a/src/components/Search/useSearchAutocompleteTransition/index.ts b/src/components/Search/DeferredSearchAutocompleteList/index.tsx similarity index 50% rename from src/components/Search/useSearchAutocompleteTransition/index.ts rename to src/components/Search/DeferredSearchAutocompleteList/index.tsx index d36f526e3340a..bf37646cf0a30 100644 --- a/src/components/Search/useSearchAutocompleteTransition/index.ts +++ b/src/components/Search/DeferredSearchAutocompleteList/index.tsx @@ -1,9 +1,6 @@ /** * The rendering is fast enough for the web, so only a native implementation is required. See index.native.ts. */ +import SearchAutocompleteList from '@components/Search/SearchAutocompleteList'; -function useSearchAutocompleteTransition() { - return true; -} - -export default useSearchAutocompleteTransition; +export default SearchAutocompleteList; diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 6cee4176edefb..bf9dd1c307f23 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -4,7 +4,6 @@ import type {ForwardedRef, RefObject} from 'react'; import React, {useEffect, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useOptionsList} from '@components/OptionListContextProvider'; -import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {ListItem as NewListItem, UserListItemProps} from '@components/SelectionList/ListItem/types'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; @@ -55,7 +54,6 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import {getSubstitutionMapKey} from './SearchRouter/getQueryWithSubstitutions'; import type {SearchFilterKey, UserFriendlyKey} from './types'; -import useSearchAutocompleteTransition from './useSearchAutocompleteTransition'; type AutocompleteItemData = { filterKey: UserFriendlyKey; @@ -916,20 +914,7 @@ function SearchAutocompleteList({ }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]); const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); - - const shouldRender = useSearchAutocompleteTransition(); - - const isLoading = !isRecentSearchesDataLoaded || !areOptionsInitialized || !shouldRender; - - if (isLoading) { - return ( - - ); - } + const isLoading = !isRecentSearchesDataLoaded || !areOptionsInitialized; return ( @@ -947,6 +932,7 @@ function SearchAutocompleteList({ shouldSingleExecuteRowSelect ref={setListRef} initialScrollIndex={0} + isLoadingNewOptions={isLoading} initiallyFocusedItemKey={!shouldUseNarrowLayout ? firstRecentReportKey : undefined} shouldScrollToFocusedIndex={!isInitialRender} disableKeyboardShortcuts={!shouldSubscribeToArrowKeyEvents} @@ -964,4 +950,4 @@ SearchAutocompleteList.displayName = 'SearchAutocompleteList'; export default React.memo(SearchAutocompleteList); export {SearchRouterItem}; -export type {GetAdditionalSectionsCallback}; +export type {GetAdditionalSectionsCallback, SearchAutocompleteListProps}; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index c9a262b3a2230..019ba2bb43a57 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -6,8 +6,8 @@ import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import DeferredAutocompleteList from '@components/Search/DeferredSearchAutocompleteList'; import type {GetAdditionalSectionsCallback} from '@components/Search/SearchAutocompleteList'; -import SearchAutocompleteList from '@components/Search/SearchAutocompleteList'; import {useSearchActionsContext} from '@components/Search/SearchContext'; import SearchInputSelectionWrapper from '@components/Search/SearchInputSelectionWrapper'; import type {SearchQueryString} from '@components/Search/types'; @@ -455,7 +455,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla shouldDelayFocus /> - { - Navigation.isNavigationReady().then(() => { - setShouldRender(true); - }); - }, []); - - return deferredValue; -} - -export default useSearchAutocompleteTransition; From fb9ace77d21660823bccabba7c4663973274bf3e Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Thu, 26 Feb 2026 17:22:26 +0100 Subject: [PATCH 3/3] refactor: get rid of deferred value --- .../DeferredSearchAutocompleteList/index.native.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx b/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx index bd18308989aa8..0d0f8771b60ac 100644 --- a/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx +++ b/src/components/Search/DeferredSearchAutocompleteList/index.native.tsx @@ -1,4 +1,4 @@ -import React, {useDeferredValue, useEffect} from 'react'; +import React from 'react'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import type {SearchAutocompleteListProps} from '@components/Search/SearchAutocompleteList'; import SearchAutocompleteList from '@components/Search/SearchAutocompleteList'; @@ -12,15 +12,14 @@ import CONST from '@src/CONST'; */ function DeferredAutocompleteList(props: SearchAutocompleteListProps) { const [shouldRender, setShouldRender] = React.useState(false); - const deferredShouldRender = useDeferredValue(shouldRender); - useEffect(() => { - Navigation.isNavigationReady().then(() => { + React.useEffect(() => { + Navigation.setNavigationActionToMicrotaskQueue(() => { setShouldRender(true); }); }, []); - if (!deferredShouldRender) { + if (!shouldRender) { return (