Skip to content

Commit b5b383b

Browse files
authored
Merge pull request #85922 from hoangzinh/feature/82187-migrate-searchautocompletelist-to-usefiltered-options
Migrate SearchAutocompleteList to useFilteredOptions
2 parents b692949 + 2ca574d commit b5b383b

8 files changed

Lines changed: 51 additions & 46 deletions

File tree

src/components/Search/SearchAutocompleteList.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type {ForwardedRef, RefObject} from 'react';
2-
import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';
2+
import React, {useEffect, useMemo, useRef, useState} from 'react';
33
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
4-
import {OptionsListStateContext, useOptionsList} from '@components/OptionListContextProvider';
54
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
65
import type {AnimatedTextInputRef} from '@components/RNTextInput';
76
import type {ListItem as NewListItem, UserListItemProps} from '@components/SelectionList/ListItem/types';
@@ -13,6 +12,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
1312
import useDebounce from '@hooks/useDebounce';
1413
import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement';
1514
import useFeedKeysWithAssignedCards from '@hooks/useFeedKeysWithAssignedCards';
15+
import useFilteredOptions from '@hooks/useFilteredOptions';
1616
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
1717
import useLocalize from '@hooks/useLocalize';
1818
import useOnyx from '@hooks/useOnyx';
@@ -94,6 +94,11 @@ const defaultListOptions = {
9494
categoryOptions: [],
9595
};
9696

97+
const emptyOptionList = {
98+
reports: [],
99+
personalDetails: [],
100+
};
101+
97102
const setPerformanceTimersEnd = () => {
98103
endSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER);
99104
};
@@ -165,7 +170,7 @@ function SearchAutocompleteList({
165170
const expensifyIcons = useMemoizedLazyExpensifyIcons(['History', 'MagnifyingGlass']);
166171
const taxRates = getAllTaxRates(policies);
167172

168-
const {options, areOptionsInitialized} = useOptionsList();
173+
const {options: listOptions, isLoading: isLoadingOptions} = useFilteredOptions({enabled: true, isSearching: !!autocompleteQueryValue.trim(), betas: betas ?? []});
169174

170175
const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata);
171176

@@ -175,25 +180,24 @@ function SearchAutocompleteList({
175180
};
176181
}, []);
177182

178-
const {areOptionsInitialized: contextAreOptionsInitialized} = useContext(OptionsListStateContext);
179183
const coldStartAttributeSet = useRef(false);
180184
useEffect(() => {
181185
if (coldStartAttributeSet.current) {
182186
return;
183187
}
184188
const parentSpan = getSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER);
185189
if (parentSpan) {
186-
parentSpan.setAttribute(CONST.TELEMETRY.ATTRIBUTE_COLD_START, !contextAreOptionsInitialized);
190+
parentSpan.setAttribute(CONST.TELEMETRY.ATTRIBUTE_COLD_START, isLoadingOptions);
187191
coldStartAttributeSet.current = true;
188192
}
189-
}, [contextAreOptionsInitialized]);
193+
}, [isLoadingOptions]);
190194

191195
const searchOptions = (() => {
192-
if (!areOptionsInitialized) {
196+
if (listOptions === null) {
193197
return defaultListOptions;
194198
}
195199
return getSearchOptions({
196-
options,
200+
options: listOptions,
197201
draftComments,
198202
nvpDismissedProductTraining,
199203
betas: betas ?? [],
@@ -277,7 +281,7 @@ function SearchAutocompleteList({
277281
autocompleteQueryValue,
278282
allCards,
279283
allFeeds,
280-
options,
284+
options: listOptions ?? emptyOptionList,
281285
draftComments,
282286
nvpDismissedProductTraining,
283287
betas,
@@ -357,7 +361,7 @@ function SearchAutocompleteList({
357361
const reasonAttributes: SkeletonSpanReasonAttributes = {
358362
context: 'SearchAutocompleteList',
359363
isRecentSearchesDataLoaded,
360-
areOptionsInitialized,
364+
isLoadingOptions,
361365
};
362366

363367
/* Sections generation */
@@ -404,7 +408,7 @@ function SearchAutocompleteList({
404408
} as AutocompleteListItem;
405409
});
406410

407-
if (areOptionsInitialized) {
411+
if (!isLoadingOptions) {
408412
pushSection({
409413
title: autocompleteQueryValue.trim() === '' ? translate('search.recentChats') : undefined,
410414
data: nextStyledRecentReports,
@@ -430,7 +434,7 @@ function SearchAutocompleteList({
430434
reasonAttributes={{
431435
context: 'SearchAutocompleteList',
432436
isRecentSearchesDataLoaded,
433-
areOptionsInitialized,
437+
isLoadingOptions,
434438
}}
435439
/>
436440
),
@@ -465,7 +469,7 @@ function SearchAutocompleteList({
465469
searchQueryItem,
466470
styles,
467471
translate,
468-
areOptionsInitialized,
472+
isLoadingOptions,
469473
isRecentSearchesDataLoaded,
470474
]);
471475

@@ -507,13 +511,13 @@ function SearchAutocompleteList({
507511
// because useState(initialFocusedIndex) in useArrowKeyFocusManager only reads the initial value.
508512
// Imperatively focus the first recent report once options become available (desktop only).
509513
useEffect(() => {
510-
if (shouldUseNarrowLayout || !areOptionsInitialized || hasSetInitialFocusRef.current || firstRecentReportFlatIndex === -1) {
514+
if (shouldUseNarrowLayout || isLoadingOptions || hasSetInitialFocusRef.current || firstRecentReportFlatIndex === -1) {
511515
return;
512516
}
513517
hasSetInitialFocusRef.current = true;
514518

515519
innerListRef.current?.updateAndScrollToFocusedIndex(firstRecentReportFlatIndex, false);
516-
}, [areOptionsInitialized, firstRecentReportFlatIndex, shouldUseNarrowLayout]);
520+
}, [isLoadingOptions, firstRecentReportFlatIndex, shouldUseNarrowLayout]);
517521

518522
useEffect(() => {
519523
const targetText = autocompleteQueryValue;

src/hooks/useFilteredOptions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ type UseFilteredOptionsConfig = {
1919
batchSize?: number;
2020
/** Whether to enable dynamic loading/pagination (default: true) */
2121
enablePagination?: boolean;
22-
/** Search term for filtering - when present, builds full report map for personal details (default: '') */
23-
searchTerm?: string;
22+
/** Whether search mode is active - when true, builds full report map for personal details (default: false) */
23+
isSearching?: boolean;
2424
/** Beta features the user has access to */
2525
betas?: OnyxEntry<Beta[]>;
2626
};
@@ -66,7 +66,7 @@ type UseFilteredOptionsResult = {
6666
* />
6767
*/
6868
function useFilteredOptions(config: UseFilteredOptionsConfig = {}): UseFilteredOptionsResult {
69-
const {maxRecentReports = 500, enabled = true, includeP2P = true, batchSize = 100, searchTerm = '', betas} = config;
69+
const {maxRecentReports = 500, enabled = true, includeP2P = true, batchSize = 100, isSearching = false, betas} = config;
7070

7171
const [reportsLimit, setReportsLimit] = useState(maxRecentReports);
7272

@@ -86,11 +86,11 @@ function useFilteredOptions(config: UseFilteredOptionsConfig = {}): UseFilteredO
8686
? createFilteredOptionList(allPersonalDetails, allReports, reportAttributesDerived, privateIsArchivedMap, allPolicies, {
8787
maxRecentReports: reportsLimit,
8888
includeP2P,
89-
searchTerm,
89+
isSearching,
9090
betas,
9191
})
9292
: null,
93-
[enabled, allReports, allPersonalDetails, reportAttributesDerived, privateIsArchivedMap, allPolicies, reportsLimit, includeP2P, searchTerm, betas],
93+
[enabled, allReports, allPersonalDetails, reportAttributesDerived, privateIsArchivedMap, allPolicies, reportsLimit, includeP2P, isSearching, betas],
9494
);
9595

9696
const hasMore = options ? reportsLimit < totalReports : false;

src/libs/OptionsListUtils/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,14 +1536,13 @@ function createFilteredOptionList(
15361536
options: {
15371537
maxRecentReports?: number;
15381538
includeP2P?: boolean;
1539-
searchTerm?: string;
1539+
isSearching?: boolean;
15401540
betas?: OnyxEntry<Beta[]>;
15411541
} = {},
15421542
policyTags?: OnyxCollection<PolicyTagLists>,
15431543
visibleReportActionsData: VisibleReportActionsDerivedValue = {},
15441544
) {
1545-
const {maxRecentReports = 500, includeP2P = true, searchTerm = ''} = options;
1546-
const isSearching = !!searchTerm?.trim();
1545+
const {maxRecentReports = 500, includeP2P = true, isSearching = false} = options;
15471546
const reportMapForAccountIDs: Record<number, Report> = {};
15481547

15491548
// Step 1: Pre-filter reports to avoid processing thousands

src/pages/NewChatPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor
9090
includeP2P: true,
9191
batchSize: 100,
9292
enablePagination: true,
93-
searchTerm: debouncedSearchTerm,
93+
isSearching: !!debouncedSearchTerm.trim(),
9494
betas,
9595
});
9696

src/pages/Share/ShareTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function ShareTab({ref}: ShareTabProps) {
6666
const {options: listOptions, isLoading} = useFilteredOptions({
6767
enabled: didScreenTransitionEnd,
6868
betas: betas ?? [],
69-
searchTerm: debouncedTextInputValue,
69+
isSearching: !!debouncedTextInputValue.trim(),
7070
});
7171
const areOptionsInitialized = !isLoading;
7272
const [isSearchingForReports] = useOnyx(ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS);

tests/perf-test/OptionsListUtils.perf-test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,26 +274,26 @@ describe('OptionsListUtils', () => {
274274
await measureFunction(() =>
275275
createFilteredOptionList(personalDetails, mockedReportsMap, undefined, EMPTY_PRIVATE_IS_ARCHIVED_MAP, undefined, {
276276
maxRecentReports: 500,
277-
searchTerm: '',
277+
isSearching: false,
278278
}),
279279
);
280280
});
281281

282-
test('[OptionsListUtils] createFilteredOptionList with searchTerm', async () => {
282+
test('[OptionsListUtils] createFilteredOptionList with isSearching is true', async () => {
283283
await waitForBatchedUpdates();
284284
await measureFunction(() =>
285285
createFilteredOptionList(personalDetails, mockedReportsMap, undefined, EMPTY_PRIVATE_IS_ARCHIVED_MAP, undefined, {
286286
maxRecentReports: 500,
287-
searchTerm: SEARCH_VALUE,
287+
isSearching: true,
288288
}),
289289
);
290290
});
291291

292-
test('[OptionsListUtils] getSearchOptions with searchTerm', async () => {
292+
test('[OptionsListUtils] getSearchOptions with isSearching is true', async () => {
293293
await waitForBatchedUpdates();
294294
const optionLists = createFilteredOptionList(personalDetails, mockedReportsMap, undefined, EMPTY_PRIVATE_IS_ARCHIVED_MAP, undefined, {
295295
maxRecentReports: 500,
296-
searchTerm: SEARCH_VALUE,
296+
isSearching: true,
297297
});
298298

299299
await measureFunction(() =>

tests/ui/components/SearchAutocompleteListTest.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,20 @@ jest.mock('@hooks/useResponsiveLayout', () => ({
3434
})),
3535
}));
3636

37-
jest.mock('@components/OptionListContextProvider', () => {
38-
const ActualReact = jest.requireActual<typeof React>('react');
39-
return {
40-
useOptionsList: jest.fn(() => ({
41-
options: {},
42-
areOptionsInitialized: true,
43-
})),
44-
OptionsListStateContext: ActualReact.createContext({
45-
areOptionsInitialized: true,
46-
}),
47-
};
48-
});
37+
jest.mock('@hooks/useFilteredOptions', () => ({
38+
// eslint-disable-next-line @typescript-eslint/naming-convention
39+
__esModule: true,
40+
default: jest.fn(() => ({
41+
options: {
42+
reports: [],
43+
personalDetails: [],
44+
},
45+
isLoading: false,
46+
loadMore: jest.fn(),
47+
hasMore: false,
48+
isLoadingMore: false,
49+
})),
50+
}));
4951

5052
jest.mock('@libs/OptionsListUtils', () => ({
5153
getSearchOptions: jest.fn(() => ({

tests/unit/OptionsListUtilsTest.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7045,22 +7045,22 @@ describe('OptionsListUtils', () => {
70457045
expect(result).toBeDefined();
70467046
});
70477047

7048-
it('should handle searchTerm filtering', () => {
7049-
const result = createFilteredOptionList(PERSONAL_DETAILS, REPORTS, undefined, {}, undefined, {searchTerm: 'Spider'});
7048+
it('should handle isSearching filtering', () => {
7049+
const result = createFilteredOptionList(PERSONAL_DETAILS, REPORTS, undefined, {}, undefined, {isSearching: true});
70507050

70517051
expect(result).toBeDefined();
70527052
expect(result.reports.length).toBe(Object.keys(REPORTS).length);
70537053
});
70547054

7055-
it('should return all reports when searchTerm is provided (isSearching is true)', () => {
7055+
it('should return all reports when isSearching is true', () => {
70567056
const result = createFilteredOptionList(
70577057
PERSONAL_DETAILS,
70587058
REPORTS,
70597059
undefined,
70607060
{},
70617061
{},
70627062
{
7063-
searchTerm: 'Report',
7063+
isSearching: true,
70647064
maxRecentReports: 2,
70657065
},
70667066
);

0 commit comments

Comments
 (0)