Skip to content

Commit 53d9bc4

Browse files
authored
Merge pull request #60264 from callstack-internal/perf/recalculate-changed-options
2 parents e6c08b6 + 9d61b4a commit 53d9bc4

2 files changed

Lines changed: 101 additions & 44 deletions

File tree

src/components/OptionListContextProvider.tsx

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
22
import {useOnyx} from 'react-native-onyx';
33
import usePrevious from '@hooks/usePrevious';
4-
import {createOptionFromReport, createOptionList} from '@libs/OptionsListUtils';
4+
import {createOptionFromReport, createOptionList, processReport} from '@libs/OptionsListUtils';
55
import type {OptionList, SearchOption} from '@libs/OptionsListUtils';
66
import {isSelfDM} from '@libs/ReportUtils';
77
import ONYXKEYS from '@src/ONYXKEYS';
@@ -46,34 +46,80 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
4646
reports: [],
4747
personalDetails: [],
4848
});
49-
const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {canBeMissing: true});
50-
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
51-
49+
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true});
50+
const prevReportAttributesLocale = usePrevious(reportAttributes?.locale);
51+
const [reports, {sourceValue: changedReports}] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
5252
const personalDetails = usePersonalDetails();
5353
const prevPersonalDetails = usePrevious(personalDetails);
54+
const hasInitialData = useMemo(() => Object.keys(personalDetails ?? {}).length > 0, [personalDetails]);
55+
56+
const loadOptions = useCallback(() => {
57+
const optionLists = createOptionList(personalDetails, reports);
58+
setOptions({
59+
reports: optionLists.reports,
60+
personalDetails: optionLists.personalDetails,
61+
});
62+
}, [personalDetails, reports]);
5463

5564
/**
56-
* This effect is used to update the options list when reports change.
65+
* This effect is responsible for generating the options list when their data is not yet initialized
5766
*/
5867
useEffect(() => {
59-
// there is no need to update the options if the options are not initialized
60-
if (!areOptionsInitialized.current || !reports) {
68+
if (!areOptionsInitialized.current || !reports || hasInitialData) {
69+
return;
70+
}
71+
72+
loadOptions();
73+
}, [reports, personalDetails, hasInitialData, loadOptions]);
74+
75+
/**
76+
* This effect is responsible for generating the options list when the locale changes
77+
* Since options might use report attributes, it's necessary to call this after report attributes are loaded with the new locale to make sure the options are generated in a proper language
78+
*/
79+
useEffect(() => {
80+
if (reportAttributes?.locale === prevReportAttributesLocale) {
81+
return;
82+
}
83+
84+
loadOptions();
85+
}, [prevReportAttributesLocale, loadOptions, reportAttributes?.locale]);
86+
87+
/**
88+
* This effect is responsible for updating the options only for changed reports
89+
*/
90+
useEffect(() => {
91+
if (!changedReports || !areOptionsInitialized.current) {
6192
return;
6293
}
63-
// Since reports updates can happen in bulk, and some reports depend on other reports, we need to recreate the whole list from scratch.
64-
const newReports = createOptionList(personalDetails, reports).reports;
6594

6695
setOptions((prevOptions) => {
67-
const newOptions = {
96+
const changedReportEntries = Object.values(changedReports);
97+
if (changedReportEntries.length === 0) {
98+
return prevOptions;
99+
}
100+
101+
const updatedReportsMap = new Map(prevOptions.reports.map((report) => [report.reportID, report]));
102+
changedReportEntries.forEach((report) => {
103+
if (!report) {
104+
return;
105+
}
106+
107+
const reportID = report?.reportID;
108+
const {reportOption} = processReport(report, personalDetails);
109+
110+
if (reportOption) {
111+
updatedReportsMap.set(reportID, reportOption);
112+
} else {
113+
updatedReportsMap.delete(reportID);
114+
}
115+
});
116+
117+
return {
68118
...prevOptions,
69-
reports: newReports,
119+
reports: Array.from(updatedReportsMap.values()),
70120
};
71-
72-
return newOptions;
73121
});
74-
75-
// eslint-disable-next-line react-compiler/react-compiler
76-
}, [reports, personalDetails, preferredLocale]);
122+
}, [changedReports, personalDetails]);
77123

78124
/**
79125
* This effect is used to update the options list when personal details change.
@@ -142,14 +188,6 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
142188
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
143189
}, [personalDetails]);
144190

145-
const loadOptions = useCallback(() => {
146-
const optionLists = createOptionList(personalDetails, reports);
147-
setOptions({
148-
reports: optionLists.reports,
149-
personalDetails: optionLists.personalDetails,
150-
});
151-
}, [personalDetails, reports]);
152-
153191
const initializeOptions = useCallback(() => {
154192
loadOptions();
155193
areOptionsInitialized.current = true;
@@ -182,7 +220,7 @@ const useOptionsListContext = () => useContext(OptionsListContext);
182220
const useOptionsList = (options?: {shouldInitialize: boolean}) => {
183221
const {shouldInitialize = true} = options ?? {};
184222
const {initializeOptions, options: optionsList, areOptionsInitialized, resetOptions} = useOptionsListContext();
185-
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
223+
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false});
186224

187225
useEffect(() => {
188226
if (!shouldInitialize || areOptionsInitialized || isLoadingApp) {

src/libs/OptionsListUtils.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,35 +1123,53 @@ function isReportSelected(reportOption: OptionData, selectedOptions: Array<Parti
11231123
return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID));
11241124
}
11251125

1126+
function processReport(
1127+
report: OnyxEntry<Report>,
1128+
personalDetails: OnyxEntry<PersonalDetailsList>,
1129+
): {
1130+
reportMapEntry?: [number, Report]; // The entry to add to reportMapForAccountIDs if applicable
1131+
reportOption: SearchOption<Report> | null; // The report option to add to allReportOptions if applicable
1132+
} {
1133+
if (!report) {
1134+
return {reportOption: null};
1135+
}
1136+
1137+
const isOneOnOneChat = reportUtilsIsOneOnOneChat(report);
1138+
const accountIDs = getParticipantsAccountIDsForDisplay(report);
1139+
const isChatRoom = reportUtilsIsChatRoom(report);
1140+
1141+
if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) {
1142+
return {reportOption: null};
1143+
}
1144+
1145+
// Determine if this report should be mapped to a personal detail
1146+
const reportMapEntry = accountIDs.length <= 1 && isOneOnOneChat ? ([accountIDs.at(0), report] as [number, Report]) : undefined;
1147+
1148+
return {
1149+
reportMapEntry,
1150+
reportOption: {
1151+
item: report,
1152+
...createOption(accountIDs, personalDetails, report, {}),
1153+
},
1154+
};
1155+
}
1156+
11261157
function createOptionList(personalDetails: OnyxEntry<PersonalDetailsList>, reports?: OnyxCollection<Report>) {
11271158
const reportMapForAccountIDs: Record<number, Report> = {};
11281159
const allReportOptions: Array<SearchOption<Report>> = [];
11291160

11301161
if (reports) {
11311162
Object.values(reports).forEach((report) => {
1132-
if (!report) {
1133-
return;
1134-
}
1135-
1136-
const isOneOnOneChat = reportUtilsIsOneOnOneChat(report);
1137-
const accountIDs = getParticipantsAccountIDsForDisplay(report);
1163+
const {reportMapEntry, reportOption} = processReport(report, personalDetails);
11381164

1139-
const isChatRoom = reportUtilsIsChatRoom(report);
1140-
if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) {
1141-
return;
1165+
if (reportMapEntry) {
1166+
const [accountID, reportValue] = reportMapEntry;
1167+
reportMapForAccountIDs[accountID] = reportValue;
11421168
}
11431169

1144-
// Save the report in the map if this is a single participant so we can associate the reportID with the
1145-
// personal detail option later. Individuals should not be associated with single participant
1146-
// policyExpenseChats or chatRooms since those are not people.
1147-
if (accountIDs.length <= 1 && isOneOnOneChat) {
1148-
reportMapForAccountIDs[accountIDs[0]] = report;
1170+
if (reportOption) {
1171+
allReportOptions.push(reportOption);
11491172
}
1150-
1151-
allReportOptions.push({
1152-
item: report,
1153-
...createOption(accountIDs, personalDetails, report, {}),
1154-
});
11551173
});
11561174
}
11571175

@@ -2383,6 +2401,7 @@ export {
23832401
getIsUserSubmittedExpenseOrScannedReceipt,
23842402
getManagerMcTestParticipant,
23852403
shouldShowLastActorDisplayName,
2404+
processReport,
23862405
};
23872406

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

0 commit comments

Comments
 (0)