Skip to content

Commit 4b68aa8

Browse files
authored
Merge pull request Expensify#66938 from callstack-internal/perf/user-select-popup-data-flow
Memoize getValidOptions across components
2 parents 5a50121 + efe37e5 commit 4b68aa8

8 files changed

Lines changed: 75 additions & 24 deletions

File tree

src/components/Search/FilterDropdowns/UserSelectPopup.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
1212
import useThemeStyles from '@hooks/useThemeStyles';
1313
import useWindowDimensions from '@hooks/useWindowDimensions';
1414
import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
15+
import memoize from '@libs/memoize';
1516
import type {Option, Section} from '@libs/OptionsListUtils';
1617
import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils';
1718
import type {OptionData} from '@libs/ReportUtils';
@@ -22,6 +23,8 @@ function getSelectedOptionData(option: Option) {
2223
return {...option, reportID: `${option.reportID}`, selected: true};
2324
}
2425

26+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'UserSelectPopup.getValidOptions'});
27+
2528
type UserSelectPopupProps = {
2629
/** The currently selected users */
2730
value: string[];
@@ -63,28 +66,29 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps)
6366

6467
const cleanSearchTerm = searchTerm.trim().toLowerCase();
6568

66-
// Get a list of all options/personal details and filter them by the current search term
67-
const listData = useMemo(() => {
68-
const optionsList = getValidOptions(
69+
const optionsList = useMemo(() => {
70+
return memoizedGetValidOptions(
6971
{
7072
reports: options.reports,
7173
personalDetails: options.personalDetails,
7274
},
7375
{
74-
selectedOptions,
7576
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
76-
includeSelectedOptions: true,
7777
includeCurrentUser: true,
7878
},
7979
);
80+
}, [options.reports, options.personalDetails]);
8081

81-
const {personalDetails: filteredOptionsList, recentReports} = filterAndOrderOptions(optionsList, cleanSearchTerm, {
82+
const filteredOptions = useMemo(() => {
83+
return filterAndOrderOptions(optionsList, cleanSearchTerm, {
8284
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
8385
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
8486
canInviteUser: false,
8587
});
88+
}, [optionsList, cleanSearchTerm]);
8689

87-
const personalDetailList = filteredOptionsList
90+
const listData = useMemo(() => {
91+
const personalDetailList = filteredOptions.personalDetails
8892
.map((participant) => ({
8993
...participant,
9094
isSelected: selectedOptions.some((selectedOption) => selectedOption.accountID === participant.accountID),
@@ -100,8 +104,16 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps)
100104
return 0;
101105
});
102106

103-
return [...(personalDetailList ?? []), ...(recentReports ?? [])];
104-
}, [cleanSearchTerm, options.personalDetails, options.reports, selectedOptions, accountID]);
107+
const recentReportsList = filteredOptions.recentReports.map((report) => {
108+
const isSelected = selectedOptions.some((selectedOption) => selectedOption.reportID === report.reportID);
109+
return {
110+
...report,
111+
isSelected,
112+
};
113+
});
114+
115+
return [...personalDetailList, ...recentReportsList];
116+
}, [filteredOptions, selectedOptions, accountID]);
105117

106118
const {sections, headerMessage} = useMemo(() => {
107119
const newSections: Section[] = [

src/components/Search/SearchFiltersParticipantsSelector.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import useLocalize from '@hooks/useLocalize';
77
import useOnyx from '@hooks/useOnyx';
88
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
99
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
10-
import {filterAndOrderOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils';
10+
import memoize from '@libs/memoize';
11+
import {filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils';
1112
import type {Option, Section} from '@libs/OptionsListUtils';
1213
import type {OptionData} from '@libs/ReportUtils';
1314
import {getDisplayNameForParticipant} from '@libs/ReportUtils';
@@ -25,6 +26,8 @@ const defaultListOptions = {
2526
headerMessage: '',
2627
};
2728

29+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'SearchFiltersParticipantsSelector.getValidOptions'});
30+
2831
function getSelectedOptionData(option: Option): OptionData {
2932
// eslint-disable-next-line rulesdir/no-default-id-values
3033
return {...option, selected: true, reportID: option.reportID ?? '-1'};
@@ -54,26 +57,29 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
5457
return defaultListOptions;
5558
}
5659

57-
return getValidOptions(
60+
return memoizedGetValidOptions(
5861
{
5962
reports: options.reports,
6063
personalDetails: options.personalDetails,
6164
},
6265
{
63-
selectedOptions,
6466
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
6567
},
6668
);
67-
}, [areOptionsInitialized, options.personalDetails, options.reports, selectedOptions]);
69+
}, [areOptionsInitialized, options.personalDetails, options.reports]);
70+
71+
const unselectedOptions = useMemo(() => {
72+
return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID)));
73+
}, [defaultOptions, selectedOptions]);
6874

6975
const chatOptions = useMemo(() => {
70-
return filterAndOrderOptions(defaultOptions, cleanSearchTerm, {
76+
return filterAndOrderOptions(unselectedOptions, cleanSearchTerm, {
7177
selectedOptions,
7278
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
7379
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
7480
canInviteUser: false,
7581
});
76-
}, [defaultOptions, cleanSearchTerm, selectedOptions]);
82+
}, [unselectedOptions, cleanSearchTerm, selectedOptions]);
7783

7884
const {sections, headerMessage} = useMemo(() => {
7985
const newSections: Section[] = [];

src/libs/OptionsListUtils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,21 @@ function filterAndOrderOptions(options: Options, searchInputValue: string, confi
25982598
};
25992599
}
26002600

2601+
/**
2602+
* Filter out selected options from personal details and recent reports
2603+
* @param options - The options to filter
2604+
* @param selectedOptions - The selected options to filter out.
2605+
* @returns The filtered options
2606+
*/
2607+
function filterSelectedOptions(options: Options, selectedOptions: Set<number | undefined>): Options {
2608+
const filteredOptions = {
2609+
...options,
2610+
personalDetails: options.personalDetails.filter(({accountID}) => !selectedOptions.has(accountID)),
2611+
recentReports: options.recentReports.filter(({accountID}) => !selectedOptions.has(accountID)),
2612+
};
2613+
return filteredOptions;
2614+
}
2615+
26012616
function sortAlphabetically<T extends Partial<Record<TKey, string | undefined>>, TKey extends keyof T>(items: T[], key: TKey): T[] {
26022617
return items.sort((a, b) => localeCompare(a[key]?.toLowerCase() ?? '', b[key]?.toLowerCase() ?? ''));
26032618
}
@@ -2707,6 +2722,7 @@ export {
27072722
shallowOptionsListCompare,
27082723
optionsOrderBy,
27092724
recentReportComparator,
2725+
filterSelectedOptions,
27102726
};
27112727

27122728
export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, Option, OptionTree, ReportAndPersonalDetailOptions};

src/pages/NewChatPage.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import useThemeStyles from '@hooks/useThemeStyles';
2323
import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/actions/Report';
2424
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2525
import Log from '@libs/Log';
26+
import memoize from '@libs/memoize';
2627
import Navigation from '@libs/Navigation/Navigation';
2728
import type {Option, Section} from '@libs/OptionsListUtils';
2829
import {
2930
filterAndOrderOptions,
31+
filterSelectedOptions,
3032
formatSectionsFromSearchTerm,
3133
getFirstKeyForList,
3234
getHeaderMessage,
@@ -49,6 +51,8 @@ type SelectedOption = ListItem &
4951
reportID?: string;
5052
};
5153

54+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'NewChatPage.getValidOptions'});
55+
5256
function useOptions() {
5357
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
5458
const [selectedOptions, setSelectedOptions] = useState<SelectedOption[]>([]);
@@ -61,28 +65,29 @@ function useOptions() {
6165
});
6266

6367
const defaultOptions = useMemo(() => {
64-
const filteredOptions = getValidOptions(
68+
const filteredOptions = memoizedGetValidOptions(
6569
{
6670
reports: listOptions.reports ?? [],
6771
personalDetails: listOptions.personalDetails ?? [],
6872
},
6973
{
7074
betas: betas ?? [],
71-
selectedOptions,
7275
includeSelfDM: true,
7376
},
7477
);
7578
return filteredOptions;
76-
}, [betas, listOptions.personalDetails, listOptions.reports, selectedOptions]);
79+
}, [betas, listOptions.personalDetails, listOptions.reports]);
80+
81+
const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [defaultOptions, selectedOptions]);
7782

7883
const options = useMemo(() => {
79-
const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchTerm, {
84+
const filteredOptions = filterAndOrderOptions(unselectedOptions, debouncedSearchTerm, {
8085
selectedOptions,
8186
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
8287
});
8388

8489
return filteredOptions;
85-
}, [debouncedSearchTerm, defaultOptions, selectedOptions]);
90+
}, [debouncedSearchTerm, unselectedOptions, selectedOptions]);
8691
const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]);
8792
const headerMessage = useMemo(() => {
8893
return getHeaderMessage(

src/pages/iou/request/MoneyRequestAccountantSelector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import useNetwork from '@hooks/useNetwork';
1212
import useOnyx from '@hooks/useOnyx';
1313
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
1414
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
15+
import memoize from '@libs/memoize';
1516
import type {Section} from '@libs/OptionsListUtils';
1617
import {
1718
filterAndOrderOptions,
@@ -30,6 +31,8 @@ import CONST from '@src/CONST';
3031
import ONYXKEYS from '@src/ONYXKEYS';
3132
import type {Accountant} from '@src/types/onyx/IOU';
3233

34+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'MoneyRequestAccountantSelector.getValidOptions'});
35+
3336
type MoneyRequestAccountantSelectorProps = {
3437
/** Callback to request parent modal to go to next step */
3538
onFinish: (value?: string) => void;
@@ -67,7 +70,7 @@ function MoneyRequestAccountantSelector({onFinish, onAccountantSelected, iouType
6770
getEmptyOptions();
6871
}
6972

70-
const optionList = getValidOptions(
73+
const optionList = memoizedGetValidOptions(
7174
{
7275
reports: options.reports,
7376
personalDetails: options.personalDetails,

src/pages/iou/request/MoneyRequestParticipantsSelector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
3535
import getPlatform from '@libs/getPlatform';
3636
import goToSettings from '@libs/goToSettings';
3737
import {isMovingTransactionFromTrackExpense} from '@libs/IOUUtils';
38+
import memoize from '@libs/memoize';
3839
import Navigation from '@libs/Navigation/Navigation';
3940
import type {Option, SearchOption, Section} from '@libs/OptionsListUtils';
4041
import {
@@ -63,6 +64,8 @@ import type {Participant} from '@src/types/onyx/IOU';
6364
import {isEmptyObject} from '@src/types/utils/EmptyObject';
6465
import ImportContactButton from './ImportContactButton';
6566

67+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'MoneyRequestParticipantsSelector.getValidOptions'});
68+
6669
type MoneyRequestParticipantsSelectorProps = {
6770
/** Callback to request parent modal to go to next step, which should be split */
6871
onFinish?: (value?: string) => void;
@@ -169,7 +172,7 @@ function MoneyRequestParticipantsSelector(
169172
};
170173
}
171174

172-
const optionList = getValidOptions(
175+
const optionList = memoizedGetValidOptions(
173176
{
174177
reports: options.reports,
175178
personalDetails: options.personalDetails.concat(contacts),

src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import useLocalize from '@hooks/useLocalize';
1212
import useOnyx from '@hooks/useOnyx';
1313
import useThemeStyles from '@hooks/useThemeStyles';
1414
import {searchInServer} from '@libs/actions/Report';
15+
import memoize from '@libs/memoize';
1516
import Navigation from '@libs/Navigation/Navigation';
1617
import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from '@libs/OptionsListUtils';
1718
import CONST from '@src/CONST';
1819
import ONYXKEYS from '@src/ONYXKEYS';
1920
import ROUTES from '@src/ROUTES';
2021
import type {Participant} from '@src/types/onyx/IOU';
2122

23+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AddDelegatePage.getValidOptions'});
24+
2225
function useOptions() {
2326
const betas = useBetas();
2427
const [isLoading, setIsLoading] = useState(true);
@@ -39,7 +42,7 @@ function useOptions() {
3942
);
4043

4144
const defaultOptions = useMemo(() => {
42-
const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions(
45+
const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions(
4346
{
4447
reports: optionsList.reports,
4548
personalDetails: optionsList.personalDetails,

src/pages/tasks/TaskAssigneeSelectorModal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {searchInServer} from '@libs/actions/Report';
2323
import {canModifyTask, editTaskAssignee, setAssigneeValue} from '@libs/actions/Task';
2424
import {READ_COMMANDS} from '@libs/API/types';
2525
import HttpUtils from '@libs/HttpUtils';
26+
import memoize from '@libs/memoize';
2627
import Navigation from '@libs/Navigation/Navigation';
2728
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
2829
import {filterAndOrderOptions, getHeaderMessage, getValidOptions, isCurrentUser} from '@libs/OptionsListUtils';
@@ -34,14 +35,16 @@ import ROUTES from '@src/ROUTES';
3435
import type SCREENS from '@src/SCREENS';
3536
import type {Report} from '@src/types/onyx';
3637

38+
const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'TaskAssigneeSelectorModal.getValidOptions'});
39+
3740
function useOptions() {
3841
const betas = useBetas();
3942
const [isLoading, setIsLoading] = useState(true);
4043
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
4144
const {options: optionsList, areOptionsInitialized} = useOptionsList();
4245

4346
const defaultOptions = useMemo(() => {
44-
const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions(
47+
const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions(
4548
{
4649
reports: optionsList.reports,
4750
personalDetails: optionsList.personalDetails,

0 commit comments

Comments
 (0)