From a64e3a2cf7c9c4b688630957afacf6f32f353f87 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 26 Jan 2026 13:51:15 +0100 Subject: [PATCH 1/6] Remove manual memoization --- .../Security/AddDelegate/AddDelegatePage.tsx | 106 ++++++++---------- 1 file changed, 47 insertions(+), 59 deletions(-) diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 727e69690c17..cb932116d5ec 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -1,11 +1,10 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -// eslint-disable-next-line no-restricted-imports -import SelectionList from '@components/SelectionListWithSections'; -import UserListItem from '@components/SelectionListWithSections/UserListItem'; +import UserListItem from '@components/SelectionList/ListItem/UserListItem'; +import SelectionList from '@components/SelectionList/SelectionListWithSections'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useSearchSelector from '@hooks/useSearchSelector'; @@ -23,18 +22,15 @@ function AddDelegatePage() { const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); - const existingDelegates = useMemo( - () => - account?.delegatedAccess?.delegates?.reduce( - (prev, {email}) => { - // eslint-disable-next-line no-param-reassign - prev[email] = true; - return prev; - }, - {} as Record, - ) ?? {}, - [account?.delegatedAccess?.delegates], - ); + const existingDelegates = + account?.delegatedAccess?.delegates?.reduce( + (prev, {email}) => { + // eslint-disable-next-line no-param-reassign + prev[email] = true; + return prev; + }, + {} as Record, + ) ?? {}; const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized, toggleSelection} = useSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, @@ -48,51 +44,41 @@ function AddDelegatePage() { }, }); - const headerMessage = useMemo(() => { - return getHeaderMessage( - (availableOptions.recentReports?.length || 0) + (availableOptions.personalDetails?.length || 0) !== 0, - !!availableOptions.userToInvite, - debouncedSearchTerm, - countryCode, - ); - }, [availableOptions.recentReports?.length, availableOptions.personalDetails?.length, availableOptions.userToInvite, debouncedSearchTerm, countryCode]); - - const sections = useMemo(() => { - const sectionsList = []; - - sectionsList.push({ + const headerMessage = getHeaderMessage( + (availableOptions.recentReports?.length || 0) + (availableOptions.personalDetails?.length || 0) !== 0, + !!availableOptions.userToInvite, + debouncedSearchTerm, + countryCode, + ); + const sectionsList: Array<{title?: string; data: typeof availableOptions.recentReports}> = [ + { title: translate('common.recents'), data: availableOptions.recentReports, - shouldShow: availableOptions.recentReports?.length > 0, - }); - - sectionsList.push({ + }, + { title: translate('common.contacts'), data: availableOptions.personalDetails, - shouldShow: availableOptions.personalDetails?.length > 0, - }); + }, + ]; - if (availableOptions.userToInvite) { - sectionsList.push({ - title: undefined, - data: [availableOptions.userToInvite], - shouldShow: true, - }); - } + if (availableOptions.userToInvite) { + sectionsList.push({ + data: [availableOptions.userToInvite], + }); + } - return sectionsList.map((section) => ({ - ...section, - data: section.data.map((option) => ({ - ...option, - text: option.text ?? '', - alternateText: option.alternateText ?? undefined, - keyForList: option.keyForList ?? '', - isDisabled: option.isDisabled ?? undefined, - login: option.login ?? undefined, - shouldShowSubscript: option.shouldShowSubscript ?? undefined, - })), - })); - }, [availableOptions.recentReports, availableOptions.personalDetails, availableOptions.userToInvite, translate]); + const sections = sectionsList.map((section) => ({ + ...section, + data: section.data.map((option, index) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: `${option.keyForList}-${index}`, + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + })); useEffect(() => { searchInServer(debouncedSearchTerm); @@ -114,10 +100,12 @@ function AddDelegatePage() { ListItem={UserListItem} onSelectRow={toggleSelection} shouldSingleExecuteRowSelect - onChangeText={setSearchTerm} - textInputValue={searchTerm} - headerMessage={headerMessage} - textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} + textInputOptions={{ + value: searchTerm, + onChangeText: setSearchTerm, + headerMessage, + label: translate('selectionList.nameEmailOrPhoneNumber'), + }} showLoadingPlaceholder={!areOptionsInitialized} isLoadingNewOptions={!!isSearchingForReports} /> From 66007b65661518525d266922473e66fe07c46015 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 26 Jan 2026 14:38:05 +0100 Subject: [PATCH 2/6] Fix sections diplay when no data --- src/components/SelectionList/hooks/useFlattenedSections.ts | 5 +++-- src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/hooks/useFlattenedSections.ts b/src/components/SelectionList/hooks/useFlattenedSections.ts index 5eb4b65adcc2..3693410596c1 100644 --- a/src/components/SelectionList/hooks/useFlattenedSections.ts +++ b/src/components/SelectionList/hooks/useFlattenedSections.ts @@ -46,7 +46,9 @@ function useFlattenedSections(sections: Array 0) { disabledIndices.push(data.length); data.push({ type: CONST.SECTION_LIST_ITEM_TYPE.HEADER, @@ -55,7 +57,6 @@ function useFlattenedSections(sections: Array From 84127145f9a2eb32196fb0e3bf0a885aa9cd1ad0 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 26 Jan 2026 15:38:51 +0100 Subject: [PATCH 3/6] Introduce sectionIndex and remove manual memoization from TaxPicker --- .../SelectionListWithSections/types.ts | 13 +++- .../hooks/useFlattenedSections.ts | 3 +- src/components/TaxPicker.tsx | 67 ++++++++----------- src/libs/TagsOptionsListUtils.ts | 12 ++-- src/libs/TaxOptionsListUtils.ts | 19 ++---- 5 files changed, 54 insertions(+), 60 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/types.ts b/src/components/SelectionList/SelectionListWithSections/types.ts index d124d004e8f4..7ed54c534ae9 100644 --- a/src/components/SelectionList/SelectionListWithSections/types.ts +++ b/src/components/SelectionList/SelectionListWithSections/types.ts @@ -8,10 +8,13 @@ type Section = { title?: string; /** Array of items in the section */ - data?: TItem[]; + data: TItem[]; /** Whether this section is disabled */ isDisabled?: boolean; + + /** Index of the section, used to create a unique flatListKey */ + sectionIndex: number; }; /** @@ -40,7 +43,13 @@ type SectionHeader = { isDisabled: boolean; }; -type SectionListItem = TItem & {flatIndex: number; type: typeof CONST.SECTION_LIST_ITEM_TYPE.ROW}; +type SectionListItem = TItem & { + flatIndex: number; + type: typeof CONST.SECTION_LIST_ITEM_TYPE.ROW; + + /** Unique key for FlashList rendering, containing section info */ + flatListKey: string; +}; type FlattenedItem = SectionListItem | SectionHeader; diff --git a/src/components/SelectionList/hooks/useFlattenedSections.ts b/src/components/SelectionList/hooks/useFlattenedSections.ts index 3693410596c1..53fbc3ed231b 100644 --- a/src/components/SelectionList/hooks/useFlattenedSections.ts +++ b/src/components/SelectionList/hooks/useFlattenedSections.ts @@ -53,7 +53,7 @@ function useFlattenedSections(sections: Array(sections: Array; data.push(itemData); diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 246ce0b0a538..ecac1467f331 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useState} from 'react'; import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -6,7 +6,7 @@ import {shouldUseTransactionDraft} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils'; import {getTaxRatesSection} from '@libs/TaxOptionsListUtils'; -import type {Tax, TaxRatesOption} from '@libs/TaxOptionsListUtils'; +import type {TaxRatesOption} from '@libs/TaxOptionsListUtils'; import {getEnabledTaxRateCount} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {IOUAction} from '@src/CONST'; @@ -68,44 +68,33 @@ function TaxPicker({selectedTaxRate = '', policyID, transactionID, onSubmit, act const shouldShowTextInput = !isTaxRatesCountBelowThreshold; - const selectedOptions = useMemo(() => { - if (!selectedTaxRate) { - return []; + const selectedOptions = selectedTaxRate + ? [ + { + modifiedName: selectedTaxRate, + isDisabled: false, + accountID: null, + }, + ] + : []; + + const sections = getTaxRatesSection({ + policy, + searchValue, + localeCompare, + selectedOptions, + transaction: currentTransaction, + }); + + const selectedOptionKey = sections?.at(0)?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList; + + const handleSelectRow = (newSelectedOption: TaxRatesOption) => { + if (selectedOptionKey === newSelectedOption.keyForList) { + onDismiss(); + return; } - - return [ - { - modifiedName: selectedTaxRate, - isDisabled: false, - accountID: null, - }, - ]; - }, [selectedTaxRate]); - - const sections = useMemo( - () => - getTaxRatesSection({ - policy, - searchValue, - localeCompare, - selectedOptions, - transaction: currentTransaction, - }), - [searchValue, selectedOptions, policy, currentTransaction, localeCompare], - ); - - const selectedOptionKey = useMemo(() => sections?.at(0)?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); - - const handleSelectRow = useCallback( - (newSelectedOption: TaxRatesOption) => { - if (selectedOptionKey === newSelectedOption.keyForList) { - onDismiss(); - return; - } - onSubmit(newSelectedOption); - }, - [onSubmit, onDismiss, selectedOptionKey], - ); + onSubmit(newSelectedOption); + }; const textInputOptions = { label: translate('common.search'), diff --git a/src/libs/TagsOptionsListUtils.ts b/src/libs/TagsOptionsListUtils.ts index 84225e03437c..b06603b8fa77 100644 --- a/src/libs/TagsOptionsListUtils.ts +++ b/src/libs/TagsOptionsListUtils.ts @@ -89,7 +89,7 @@ function getTagListSections({ tagSections.push({ // "Selected" section title: '', - shouldShow: false, + sectionIndex: 0, data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); @@ -105,7 +105,7 @@ function getTagListSections({ tagSections.push({ // "Search" section title: '', - shouldShow: true, + sectionIndex: 1, data: getTagsOptions(tagsForSearch, selectedOptions), }); @@ -116,7 +116,7 @@ function getTagListSections({ tagSections.push({ // "All" section when items amount less than the threshold title: '', - shouldShow: false, + sectionIndex: 2, data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), }); @@ -134,7 +134,7 @@ function getTagListSections({ tagSections.push({ // "Selected" section title: '', - shouldShow: true, + sectionIndex: 3, data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); } @@ -145,7 +145,7 @@ function getTagListSections({ tagSections.push({ // "Recent" section title: translate('common.recent'), - shouldShow: true, + sectionIndex: 4, data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); } @@ -153,7 +153,7 @@ function getTagListSections({ tagSections.push({ // "All" section when items amount more than the threshold title: translate('common.all'), - shouldShow: true, + sectionIndex: 5, data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), }); diff --git a/src/libs/TaxOptionsListUtils.ts b/src/libs/TaxOptionsListUtils.ts index a163813f8c64..ce313b97a72b 100644 --- a/src/libs/TaxOptionsListUtils.ts +++ b/src/libs/TaxOptionsListUtils.ts @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import type {Section} from '@components/SelectionList/SelectionListWithSections/types'; import CONST from '@src/CONST'; import type {Policy, TaxRate, TaxRates, Transaction} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -23,12 +24,6 @@ type Tax = { isDisabled?: boolean; }; -type TaxSection = { - title: string | undefined; - shouldShow: boolean; - data: TaxRatesOption[]; -}; - /** * Sorts tax rates alphabetically by name. */ @@ -68,7 +63,7 @@ function getTaxRatesSection({ localeCompare: LocaleContextProps['localeCompare']; selectedOptions?: Tax[]; transaction?: OnyxEntry; -}): TaxSection[] { +}): Array> { const policyRatesSections = []; const taxes = transformedTaxRates(policy, transaction); @@ -94,7 +89,7 @@ function getTaxRatesSection({ policyRatesSections.push({ // "Selected" section title: '', - shouldShow: false, + sectionIndex: 0, data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); @@ -110,7 +105,7 @@ function getTaxRatesSection({ policyRatesSections.push({ // "Search" section title: '', - shouldShow: true, + sectionIndex: 1, data: getTaxRatesOptions(taxesForSearch), }); @@ -121,7 +116,7 @@ function getTaxRatesSection({ policyRatesSections.push({ // "All" section when items amount less than the threshold title: '', - shouldShow: false, + sectionIndex: 2, data: getTaxRatesOptions([...selectedTaxRateWithDisabledState, ...enabledTaxRatesWithoutSelectedOptions]), }); @@ -132,7 +127,7 @@ function getTaxRatesSection({ policyRatesSections.push({ // "Selected" section title: '', - shouldShow: true, + sectionIndex: 3, data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); } @@ -140,7 +135,7 @@ function getTaxRatesSection({ policyRatesSections.push({ // "All" section when number of items are more than the threshold title: '', - shouldShow: true, + sectionIndex: 4, data: getTaxRatesOptions(enabledTaxRatesWithoutSelectedOptions), }); From 25cfa356b23b2836c6be8f0d6c3cbd29f5a22125 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 26 Jan 2026 15:59:18 +0100 Subject: [PATCH 4/6] Adjust textoptionslistutils test --- tests/unit/TaxOptionsListUtilsTest.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/unit/TaxOptionsListUtilsTest.ts b/tests/unit/TaxOptionsListUtilsTest.ts index 3aa41744693a..53f435f2c88c 100644 --- a/tests/unit/TaxOptionsListUtilsTest.ts +++ b/tests/unit/TaxOptionsListUtilsTest.ts @@ -1,4 +1,5 @@ -import type {Section} from '@libs/OptionsListUtils'; +import type {Section} from '@components/SelectionList/SelectionListWithSections/types'; +import type {TaxRatesOption} from '@libs/TaxOptionsListUtils'; import {getTaxRatesSection} from '@libs/TaxOptionsListUtils'; import IntlStore from '@src/languages/IntlStore'; import type {Policy, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; @@ -53,7 +54,7 @@ describe('TaxOptionsListUtils', () => { taxCode: 'CODE1', } as Transaction; - const resultList: Section[] = [ + const resultList: Array> = [ { data: [ { @@ -87,12 +88,12 @@ describe('TaxOptionsListUtils', () => { pendingAction: 'delete', }, ], - shouldShow: false, + sectionIndex: 2, title: '', }, ]; - const searchResultList: Section[] = [ + const searchResultList: Array> = [ { data: [ { @@ -106,15 +107,15 @@ describe('TaxOptionsListUtils', () => { pendingAction: 'delete', }, ], - shouldShow: true, + sectionIndex: 1, title: '', }, ]; - const wrongSearchResultList: Section[] = [ + const wrongSearchResultList: Array> = [ { data: [], - shouldShow: true, + sectionIndex: 1, title: '', }, ]; From 5489c6fe839e12943ed724eda8c77b5a23e1230a Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 26 Jan 2026 16:11:49 +0100 Subject: [PATCH 5/6] Fix types on AddDelegatePage --- src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 780a4a847ac7..c48a659b2055 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -50,19 +50,23 @@ function AddDelegatePage() { debouncedSearchTerm, countryCode, ); - const sectionsList: Array<{title?: string; data: typeof availableOptions.recentReports}> = [ + const sectionsList = [ { title: translate('common.recents'), + sectionIndex: 0, data: availableOptions.recentReports, }, { title: translate('common.contacts'), + sectionIndex: 1, data: availableOptions.personalDetails, }, ]; if (availableOptions.userToInvite) { sectionsList.push({ + sectionIndex: 2, + title: '', data: [availableOptions.userToInvite], }); } From 005ef03547b8d1f781f3f4469950b1a0d49f4d50 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 26 Jan 2026 16:31:05 +0100 Subject: [PATCH 6/6] Remove unecessary changes --- src/libs/TagsOptionsListUtils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/TagsOptionsListUtils.ts b/src/libs/TagsOptionsListUtils.ts index b06603b8fa77..84225e03437c 100644 --- a/src/libs/TagsOptionsListUtils.ts +++ b/src/libs/TagsOptionsListUtils.ts @@ -89,7 +89,7 @@ function getTagListSections({ tagSections.push({ // "Selected" section title: '', - sectionIndex: 0, + shouldShow: false, data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); @@ -105,7 +105,7 @@ function getTagListSections({ tagSections.push({ // "Search" section title: '', - sectionIndex: 1, + shouldShow: true, data: getTagsOptions(tagsForSearch, selectedOptions), }); @@ -116,7 +116,7 @@ function getTagListSections({ tagSections.push({ // "All" section when items amount less than the threshold title: '', - sectionIndex: 2, + shouldShow: false, data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), }); @@ -134,7 +134,7 @@ function getTagListSections({ tagSections.push({ // "Selected" section title: '', - sectionIndex: 3, + shouldShow: true, data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); } @@ -145,7 +145,7 @@ function getTagListSections({ tagSections.push({ // "Recent" section title: translate('common.recent'), - sectionIndex: 4, + shouldShow: true, data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); } @@ -153,7 +153,7 @@ function getTagListSections({ tagSections.push({ // "All" section when items amount more than the threshold title: translate('common.all'), - sectionIndex: 5, + shouldShow: true, data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), });