Skip to content

Commit 9bde164

Browse files
authored
Merge pull request Expensify#65247 from callstack-internal/perf/report-transactions-derived-value
perf: create report transactions and violations derived value
2 parents 2e8b475 + 3d01c11 commit 9bde164

14 files changed

Lines changed: 166 additions & 53 deletions

src/ONYXKEYS.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,7 @@ const ONYXKEYS = {
849849
},
850850
DERIVED: {
851851
REPORT_ATTRIBUTES: 'reportAttributes',
852+
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
852853
},
853854
} as const;
854855

@@ -1202,6 +1203,7 @@ type OnyxValuesMapping = {
12021203

12031204
type OnyxDerivedValuesMapping = {
12041205
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue;
1206+
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue;
12051207
};
12061208

12071209
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;

src/components/MoneyRequestReportView/MoneyRequestReportView.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {PortalHost} from '@gorhom/portal';
22
import React, {useCallback, useMemo} from 'react';
33
import {InteractionManager, View} from 'react-native';
4-
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
4+
import type {OnyxEntry} from 'react-native-onyx';
55
import HeaderGap from '@components/HeaderGap';
66
import MoneyReportHeader from '@components/MoneyReportHeader';
77
import MoneyRequestHeader from '@components/MoneyRequestHeader';
@@ -13,10 +13,11 @@ import useNewTransactions from '@hooks/useNewTransactions';
1313
import useOnyx from '@hooks/useOnyx';
1414
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
1515
import useThemeStyles from '@hooks/useThemeStyles';
16+
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
1617
import {removeFailedReport} from '@libs/actions/Report';
1718
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
1819
import Log from '@libs/Log';
19-
import {selectAllTransactionsForReport, shouldDisplayReportTableView, shouldWaitForTransactions as shouldWaitForTransactionsUtil} from '@libs/MoneyRequestReportUtils';
20+
import {getAllNonDeletedTransactions, shouldDisplayReportTableView, shouldWaitForTransactions as shouldWaitForTransactionsUtil} from '@libs/MoneyRequestReportUtils';
2021
import navigationRef from '@libs/Navigation/navigationRef';
2122
import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, isMoneyRequestAction} from '@libs/ReportActionsUtils';
2223
import {canEditReportAction, getReportOfflinePendingActionAndErrors, isReportTransactionThread} from '@libs/ReportUtils';
@@ -97,10 +98,8 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
9798
const {reportActions: unfilteredReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID);
9899
const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
99100

100-
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
101-
selector: (allTransactions: OnyxCollection<OnyxTypes.Transaction>) => selectAllTransactionsForReport(allTransactions, reportID, reportActions),
102-
canBeMissing: true,
103-
});
101+
const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(reportID);
102+
const transactions = useMemo(() => getAllNonDeletedTransactions(reportTransactions, reportActions), [reportTransactions, reportActions]);
104103

105104
const reportTransactionIDs = transactions?.map((transaction) => transaction.transactionID);
106105
const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);

src/components/OnyxProvider.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const [PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKE
1313
const [FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS);
1414
const [PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
1515
const [SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION);
16+
const [ReportTransactionsAndViolationsProvider, , useAllReportsTransactionsAndViolations] = createOnyxContext(ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS);
1617

1718
type OnyxProviderProps = {
1819
/** Rendered child component */
@@ -31,6 +32,7 @@ function OnyxProvider(props: OnyxProviderProps) {
3132
FrequentlyUsedEmojisProvider,
3233
PreferredEmojiSkinToneProvider,
3334
SessionProvider,
35+
ReportTransactionsAndViolationsProvider,
3436
]}
3537
>
3638
{props.children}
@@ -52,4 +54,5 @@ export {
5254
PreferredEmojiSkinToneContext,
5355
useBlockedFromConcierge,
5456
useSession,
57+
useAllReportsTransactionsAndViolations,
5558
};
Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
1+
import {useMemo} from 'react';
12
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
2-
import {reportTransactionsSelector} from '@libs/ReportUtils';
3+
import {useAllReportsTransactionsAndViolations} from '@components/OnyxProvider';
34
import CONST from '@src/CONST';
45
import ONYXKEYS from '@src/ONYXKEYS';
56
import type {Report, Transaction, TransactionViolation} from '@src/types/onyx';
67
import useNetwork from './useNetwork';
78
import useOnyx from './useOnyx';
89

9-
const DEFAULT_TRANSACTIONS: Transaction[] = [];
10+
const DEFAULT_TRANSACTIONS: Record<string, Transaction> = {};
11+
const DEFAULT_FILTERED_TRANSACTIONS: Transaction[] = [];
1012
const DEFAULT_VIOLATIONS: Record<string, TransactionViolation[]> = {};
1113

1214
function useReportWithTransactionsAndViolations(reportID?: string): [OnyxEntry<Report>, Transaction[], OnyxCollection<TransactionViolation[]>] {
1315
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false});
14-
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
15-
selector: (_transactions) => reportTransactionsSelector(_transactions, reportID),
16-
canBeMissing: true,
17-
});
16+
17+
// It connects to single Onyx instance held in OnyxProvider, so it can be safely used in list items without affecting performance.
18+
const allReportTransactionsAndViolations = useAllReportsTransactionsAndViolations();
19+
const {transactions, violations} = allReportTransactionsAndViolations?.[reportID ?? CONST.DEFAULT_NUMBER_ID] ?? {transactions: DEFAULT_TRANSACTIONS, violations: DEFAULT_VIOLATIONS};
1820
const {isOffline} = useNetwork();
19-
const filteredTransactions = transactions?.filter((transaction) => isOffline || transaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
20-
const [violations] = useOnyx(
21-
ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
22-
{
23-
selector: (allViolations) =>
24-
Object.fromEntries(
25-
Object.entries(allViolations ?? {}).filter(([key]) =>
26-
filteredTransactions?.some((transaction) => transaction.transactionID === key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, '')),
27-
),
28-
),
29-
canBeMissing: true,
30-
},
31-
[filteredTransactions],
21+
const filteredTransactions = useMemo(
22+
() => Object.values(transactions).filter((transaction) => isOffline || transaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
23+
[transactions, isOffline],
3224
);
33-
return [report, filteredTransactions ?? DEFAULT_TRANSACTIONS, violations ?? DEFAULT_VIOLATIONS];
25+
26+
return [report, filteredTransactions ?? DEFAULT_FILTERED_TRANSACTIONS, violations];
3427
}
3528

3629
export default useReportWithTransactionsAndViolations;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {useAllReportsTransactionsAndViolations} from '@components/OnyxProvider';
2+
import CONST from '@src/CONST';
3+
4+
const DEFAULT_RETURN_VALUE = {transactions: {}, violations: {}};
5+
6+
function useTransactionsAndViolationsForReport(reportID?: string) {
7+
const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations();
8+
9+
if (!reportID) {
10+
return DEFAULT_RETURN_VALUE;
11+
}
12+
13+
return allReportsTransactionsAndViolations?.[reportID ?? CONST.DEFAULT_NUMBER_ID] ?? DEFAULT_RETURN_VALUE;
14+
}
15+
16+
export default useTransactionsAndViolationsForReport;

src/libs/MoneyRequestReportUtils.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,15 @@ function getReportIDForTransaction(transactionItem: TransactionListItemType) {
6969
}
7070

7171
/**
72-
* Filters all available transactions and returns the ones that belong to a specific report (by `reportID`).
73-
* It is used as an onyx selector, to make sure that report related views do not process all transactions in onyx.
72+
* Filters all available transactions and returns the ones that belong to not removed parent action.
7473
*/
75-
function selectAllTransactionsForReport(transactions: OnyxCollection<Transaction>, reportID: string | undefined, reportActions: ReportAction[]) {
76-
if (!reportID) {
77-
return [];
78-
}
79-
74+
function getAllNonDeletedTransactions(transactions: OnyxCollection<Transaction>, reportActions: ReportAction[]) {
8075
return Object.values(transactions ?? {}).filter((transaction): transaction is Transaction => {
8176
if (!transaction) {
8277
return false;
8378
}
8479
const action = getIOUActionForTransactionID(reportActions, transaction.transactionID);
85-
return transaction.reportID === reportID && !isDeletedParentAction(action);
80+
return !isDeletedParentAction(action);
8681
});
8782
}
8883

@@ -164,7 +159,7 @@ export {
164159
getThreadReportIDsForTransactions,
165160
getReportIDForTransaction,
166161
getTotalAmountForIOUReportPreviewButton,
167-
selectAllTransactionsForReport,
162+
getAllNonDeletedTransactions,
168163
isSingleTransactionReport,
169164
shouldDisplayReportTableView,
170165
shouldWaitForTransactions,

src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {ValueOf} from 'type-fest';
22
import ONYXKEYS from '@src/ONYXKEYS';
33
import reportAttributesConfig from './configs/reportAttributes';
4+
import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations';
45
import type {OnyxDerivedValueConfig} from './types';
56

67
/**
@@ -9,6 +10,7 @@ import type {OnyxDerivedValueConfig} from './types';
910
*/
1011
const ONYX_DERIVED_VALUES = {
1112
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig,
13+
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: reportTransactionsAndViolationsConfig,
1214
} as const satisfies {
1315
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1416
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type {OnyxCollection} from 'react-native-onyx';
2+
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
3+
import ONYXKEYS from '@src/ONYXKEYS';
4+
import type {Transaction} from '@src/types/onyx';
5+
6+
let previousTransactions: OnyxCollection<Transaction> = {};
7+
8+
export default createOnyxDerivedValueConfig({
9+
key: ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS,
10+
dependencies: [ONYXKEYS.COLLECTION.TRANSACTION, ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS],
11+
compute: ([transactions, violations], {sourceValues, currentValue}) => {
12+
if (!transactions) {
13+
return {};
14+
}
15+
16+
// If there is a source value for transactions or transaction violations, we need to process only the transactions that have been updated or added
17+
// If not, we need to process all transactions
18+
const transactionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION];
19+
const transactionViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS];
20+
let transactionsToProcess = Object.keys(transactions);
21+
if (transactionsUpdates) {
22+
transactionsToProcess = Object.keys(transactionsUpdates);
23+
} else if (transactionViolationsUpdates) {
24+
transactionsToProcess = Object.keys(transactionViolationsUpdates).map((transactionViolation) =>
25+
transactionViolation.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, ONYXKEYS.COLLECTION.TRANSACTION),
26+
);
27+
}
28+
29+
const reportTransactionsAndViolations = currentValue ?? {};
30+
for (const transactionKey of transactionsToProcess) {
31+
const transaction = transactions[transactionKey];
32+
const reportID = transaction?.reportID;
33+
34+
// If the reportID of the transaction has changed (e.g. the transaction was split into multiple reports), we need to delete the transaction from the previous reportID and the violations from the previous reportID
35+
const previousTransaction = previousTransactions?.[transactionKey];
36+
const previousReportID = previousTransaction?.reportID;
37+
38+
if (previousReportID && previousReportID !== reportID && reportTransactionsAndViolations[previousReportID]) {
39+
delete reportTransactionsAndViolations[previousReportID].transactions[transactionKey];
40+
const transactionID = previousTransaction?.transactionID;
41+
if (transactionID) {
42+
delete reportTransactionsAndViolations[previousReportID].violations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
43+
}
44+
}
45+
46+
if (!reportID) {
47+
// eslint-disable-next-line no-continue
48+
continue;
49+
}
50+
51+
if (!reportTransactionsAndViolations[reportID]) {
52+
reportTransactionsAndViolations[reportID] = {
53+
transactions: {},
54+
violations: {},
55+
};
56+
}
57+
58+
const transactionID = transaction.transactionID;
59+
const transactionViolations = violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
60+
61+
if (transactionViolations && transactionViolations.length > 0) {
62+
reportTransactionsAndViolations[reportID].violations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] = transactionViolations;
63+
}
64+
65+
reportTransactionsAndViolations[reportID].transactions[transactionKey] = transaction;
66+
}
67+
68+
previousTransactions = transactions;
69+
70+
return reportTransactionsAndViolations;
71+
},
72+
});

src/libs/actions/OnyxDerived/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function init() {
3939
sourceValues: undefined,
4040
areAllConnectionsSet: false,
4141
};
42+
// @ts-expect-error TypeScript can't confirm the shape of dependencyValues matches the compute function's parameters
4243
derivedValue = compute(dependencyValues, initialContext);
4344
dependencyValues = values;
4445
Onyx.set(key, derivedValue ?? null);
@@ -79,6 +80,7 @@ function init() {
7980
[sourceKey]: sourceValue,
8081
};
8182
}
83+
// @ts-expect-error TypeScript can't confirm the shape of dependencyValues matches the compute function's parameters
8284
const newDerivedValue = compute(dependencyValues, context);
8385
Log.info(`[OnyxDerived] updating value for ${key} in Onyx`);
8486
derivedValue = newDerivedValue;

src/pages/home/ReportScreen.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ import usePermissions from '@hooks/usePermissions';
2828
import usePrevious from '@hooks/usePrevious';
2929
import useResponsiveLayout from '@hooks/useResponsiveLayout';
3030
import useThemeStyles from '@hooks/useThemeStyles';
31+
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
3132
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
3233
import {hideEmojiPicker} from '@libs/actions/EmojiPickerAction';
3334
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
3435
import Log from '@libs/Log';
35-
import {selectAllTransactionsForReport, shouldDisplayReportTableView, shouldWaitForTransactions as shouldWaitForTransactionsUtil} from '@libs/MoneyRequestReportUtils';
36+
import {getAllNonDeletedTransactions, shouldDisplayReportTableView, shouldWaitForTransactions as shouldWaitForTransactionsUtil} from '@libs/MoneyRequestReportUtils';
3637
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
3738
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
3839
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
@@ -296,11 +297,9 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
296297
// If the count is too high (equal to or exceeds the web pagination size / 50) and there are no cached messages in the report,
297298
// OpenReport will be called each time the user scrolls up the report a bit, clicks on report preview, and then goes back.
298299
const isLinkedMessagePageReady = isLinkedMessageAvailable && (reportActions.length - indexOfLinkedMessage >= CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || doesCreatedActionExists());
300+
const {transactions: allReportTransactions} = useTransactionsAndViolationsForReport(reportIDFromRoute);
299301

300-
const [reportTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
301-
selector: (allTransactions): OnyxTypes.Transaction[] => selectAllTransactionsForReport(allTransactions, reportIDFromRoute, reportActions),
302-
canBeMissing: false,
303-
});
302+
const reportTransactions = useMemo(() => getAllNonDeletedTransactions(allReportTransactions, reportActions), [allReportTransactions, reportActions]);
304303
const reportTransactionIDs = reportTransactions?.map((transaction) => transaction.transactionID);
305304
const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);
306305
const [transactionThreadReportActions = getEmptyObject<OnyxTypes.ReportActions>()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, {

0 commit comments

Comments
 (0)