Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,7 @@ function MoneyRequestReportTransactionList({
return groupTransactionsByTag(sortedTransactions, report, localeCompare);
}
return groupTransactionsByCategory(sortedTransactions, report, localeCompare);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedTransactions, currentGroupBy, report?.reportID, localeCompare, shouldShowGroupedTransactions]);
}, [sortedTransactions, currentGroupBy, report, localeCompare, shouldShowGroupedTransactions]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ [PERF-5] (docs)

Including the entire report object in the dependency array causes unnecessary re-renders when any property of report changes. The grouping functions only use report.currency, not the entire report object.

Suggested fix: Use specific field comparison instead of the entire object:

}, [sortedTransactions, currentGroupBy, report?.reportID, report?.currency, localeCompare, shouldShowGroupedTransactions]);

This ensures the memoization only breaks when the actually-used properties change.


Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.


const visualOrderTransactionIDs = useMemo(() => {
if (!shouldShowGroupedTransactions || groupedTransactions.length === 0) {
Expand Down
8 changes: 4 additions & 4 deletions src/components/TabSelector/TabSelectorBase.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {Animated} from 'react-native';
Expand Down Expand Up @@ -74,7 +74,7 @@ function TabSelectorBase({

const routesLength = tabs.length;

const defaultAffectedAnimatedTabs = Array.from({length: routesLength}, (_v, i) => i);
const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: routesLength}, (_v, i) => i), [routesLength]);
const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs);
const viewRef = useRef<View>(null);
const [selectorWidth, setSelectorWidth] = useState(0);
Expand All @@ -92,12 +92,12 @@ function TabSelectorBase({
return () => clearTimeout(timerID);
}, [defaultAffectedAnimatedTabs, activeIndex]);

const measure = () => {
const measure = useCallback(() => {
viewRef.current?.measureInWindow((x, _y, width) => {
setSelectorX(x);
setSelectorWidth(width);
});
};
}, []);

// Measure location/width after initial mount and when layout animations settle.
useLayoutEffect(() => {
Expand Down
31 changes: 19 additions & 12 deletions src/hooks/useTransactionViolationOfWorkspace.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useCallback} from 'react';
import {useCallback, useMemo} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import {extractCollectionItemID} from '@libs/CollectionUtils';
import {getReportTransactions, isChatRoom, isPolicyExpenseChat, isPolicyRelatedReport, isTaskReport} from '@libs/ReportUtils';
Expand All @@ -9,19 +9,26 @@ import useOnyx from './useOnyx';

function useTransactionViolationOfWorkspace(policyID?: string) {
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
const reportsToArchive = Object.values(allReports ?? {}).filter(
(report): report is Report => report != null && isPolicyRelatedReport(report, policyID) && (isChatRoom(report) || isPolicyExpenseChat(report) || isTaskReport(report)),
const reportsToArchive = useMemo(
() =>
Object.values(allReports ?? {}).filter(
(report): report is Report => report != null && isPolicyRelatedReport(report, policyID) && (isChatRoom(report) || isPolicyExpenseChat(report) || isTaskReport(report)),
),
[allReports, policyID],
);
const transactionIDSet = new Set<string>();
for (const report of reportsToArchive) {
if (!report?.iouReportID) {
continue;
}
const reportTransactions = getReportTransactions(report.iouReportID);
for (const transaction of reportTransactions) {
transactionIDSet.add(transaction.transactionID);
const transactionIDSet = useMemo(() => {
const set = new Set<string>();
for (const report of reportsToArchive) {
if (!report?.iouReportID) {
continue;
}
const reportTransactions = getReportTransactions(report.iouReportID);
for (const transaction of reportTransactions) {
set.add(transaction.transactionID);
}
}
}
return set;
}, [reportsToArchive]);
Comment on lines +19 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute transaction ID set when transactions change

Because transactionIDSet is now memoized only on reportsToArchive, it won’t update when the transactions for those reports change. If a new transaction (with a violation) is added to an existing report, useOnyx(TRANSACTION_VIOLATIONS) will re-render due to the new violation but transactionIDSet stays stale and filters it out, so the hook can miss violations for newly added transactions. Previously, the set was rebuilt on every render and picked up the latest getReportTransactions data. Consider including a dependency that changes when report transactions update, or avoid memoizing this set.

Useful? React with 👍 / 👎.


const transactionViolationSelector = useCallback(
(violations: OnyxCollection<TransactionViolations>) => {
Expand Down
2 changes: 1 addition & 1 deletion src/libs/CurrencyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {format, formatToParts} from './NumberFormatUtils';

let currencyList: OnyxValues[typeof ONYXKEYS.CURRENCY_LIST] = {};

Onyx.connect({
Onyx.connectWithoutView({
key: ONYXKEYS.CURRENCY_LIST,
callback: (val) => {
if (!val || Object.keys(val).length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/libs/LocalePhoneNumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import {parsePhoneNumber} from './PhoneNumber';

let countryCodeByIPOnyx: number;
Onyx.connect({
Onyx.connectWithoutView({
key: ONYXKEYS.COUNTRY_CODE,
callback: (val) => (countryCodeByIPOnyx = val ?? 1),
});
Expand Down
4 changes: 2 additions & 2 deletions src/libs/Middleware/Pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ function registerPaginationConfig<TResourceKey extends OnyxCollectionKey, TPageK
paginationConfigs.set(initialCommand, {...config, type: 'initial'} as unknown as PaginationConfigMapValue);
paginationConfigs.set(previousCommand, {...config, type: 'previous'} as unknown as PaginationConfigMapValue);
paginationConfigs.set(nextCommand, {...config, type: 'next'} as unknown as PaginationConfigMapValue);
Onyx.connect<OnyxCollectionKey>({
Onyx.connectWithoutView<OnyxCollectionKey>({
key: config.resourceCollectionKey,
waitForCollectionCallback: true,
callback: (data) => {
resources.set(config.resourceCollectionKey, data);
},
});
Onyx.connect<OnyxPagesKey>({
Onyx.connectWithoutView<OnyxPagesKey>({
key: config.pageCollectionKey,
waitForCollectionCallback: true,
callback: (data) => {
Expand Down
11 changes: 4 additions & 7 deletions src/pages/workspace/companyCards/addNew/CardTypeStep.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import FormHelpMessage from '@components/FormHelpMessage';
Expand Down Expand Up @@ -83,7 +83,8 @@ function CardTypeStep() {
const styles = useThemeStyles();
const companyCardBankIcons = useCompanyCardBankIcons();
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true});
const [typeSelected, setTypeSelected] = useState<CardFeedProvider>();
const [localTypeSelected, setLocalTypeSelected] = useState<CardFeedProvider>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ [PERF-6] (docs)

The removed useEffect that syncs addNewCard?.data.feedType to typeSelected state is being replaced with a derived pattern. However, the current implementation doesn't properly derive state - it still maintains typeSelected as separate state instead of computing it directly from props.

Suggested fix: Replace the local state pattern with true state derivation:

const [localTypeSelected, setLocalTypeSelected] = useState<CardFeedProvider>();
const typeSelected = localTypeSelected ?? addNewCard?.data.feedType;

This ensures typeSelected is always synchronized with Onyx without needing useEffect.


Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

const typeSelected = localTypeSelected ?? addNewCard?.data.feedType;
const [isError, setIsError] = useState(false);
const data = getAvailableCompanyCardTypes({
translate,
Expand Down Expand Up @@ -111,10 +112,6 @@ function CardTypeStep() {
}
}, [bankName, isNewCardTypeSelected, isOtherBankSelected, typeSelected]);

useEffect(() => {
setTypeSelected(addNewCard?.data.feedType);
}, [addNewCard?.data.feedType]);

const handleBackButtonPress = () => {
if (isOtherBankSelected) {
setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
Expand Down Expand Up @@ -153,7 +150,7 @@ function CardTypeStep() {
data={data}
ListItem={RadioListItem}
onSelectRow={({value}) => {
setTypeSelected(value);
setLocalTypeSelected(value);
setIsError(false);
}}
confirmButtonOptions={confirmButtonOptions}
Expand Down
13 changes: 5 additions & 8 deletions src/pages/workspace/companyCards/addNew/SelectBankStep.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useRoute} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import FormHelpMessage from '@components/FormHelpMessage';
Expand Down Expand Up @@ -36,7 +36,8 @@ function SelectBankStep() {

const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true});
const [isDebugModeEnabled = false] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED, {canBeMissing: true});
const [bankSelected, setBankSelected] = useState<ValueOf<typeof CONST.COMPANY_CARDS.BANKS> | null>();
const [localBankSelected, setLocalBankSelected] = useState<ValueOf<typeof CONST.COMPANY_CARDS.BANKS> | null>();
const bankSelected = localBankSelected ?? addNewCard?.data.selectedBank;
const [hasError, setHasError] = useState(false);
const isOtherBankSelected = bankSelected === CONST.COMPANY_CARDS.BANKS.OTHER;

Expand All @@ -56,10 +57,6 @@ function SelectBankStep() {
}
}, [bankSelected, isOtherBankSelected]);

useEffect(() => {
setBankSelected(addNewCard?.data.selectedBank);
}, [addNewCard?.data.selectedBank]);

const handleBackButtonPress = () => {
if (route?.params?.backTo) {
Navigation.navigate(route.params.backTo);
Expand Down Expand Up @@ -118,10 +115,10 @@ function SelectBankStep() {
data={data}
ListItem={RadioListItem}
onSelectRow={({value}) => {
setBankSelected(value);
setLocalBankSelected(value);
setHasError(false);
}}
initiallyFocusedItemKey={addNewCard?.data.selectedBank ?? undefined}
initiallyFocusedItemKey={bankSelected ?? undefined}
confirmButtonOptions={confirmButtonOptions}
shouldSingleExecuteRowSelect
shouldUpdateFocusedIndex
Expand Down
18 changes: 5 additions & 13 deletions src/pages/workspace/companyCards/addNew/SelectFeedType.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import FormHelpMessage from '@components/FormHelpMessage';
Expand All @@ -20,10 +20,12 @@ function SelectFeedType() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true});
const [typeSelected, setTypeSelected] = useState<ValueOf<typeof CONST.COMPANY_CARDS.FEED_TYPE>>();
const [localTypeSelected, setLocalTypeSelected] = useState<ValueOf<typeof CONST.COMPANY_CARDS.FEED_TYPE>>();
const [hasError, setHasError] = useState(false);
const doesCountrySupportPlaid = isPlaidSupportedCountry(addNewCard?.data?.selectedCountry);
const isUSCountry = addNewCard?.data?.selectedCountry === CONST.COUNTRY.US;
const defaultTypeSelected = addNewCard?.data.selectedFeedType ?? (doesCountrySupportPlaid ? CONST.COMPANY_CARDS.FEED_TYPE.DIRECT : undefined);
const typeSelected = localTypeSelected ?? defaultTypeSelected;

const submit = useCallback(() => {
if (!typeSelected) {
Expand All @@ -46,16 +48,6 @@ function SelectFeedType() {
});
}, [isUSCountry, typeSelected]);

useEffect(() => {
if (addNewCard?.data.selectedFeedType) {
setTypeSelected(addNewCard?.data.selectedFeedType);
return;
}
if (doesCountrySupportPlaid) {
setTypeSelected(CONST.COMPANY_CARDS.FEED_TYPE.DIRECT);
}
}, [addNewCard?.data.selectedFeedType, doesCountrySupportPlaid]);

const handleBackButtonPress = () => {
setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_COUNTRY});
};
Expand Down Expand Up @@ -118,7 +110,7 @@ function SelectFeedType() {
ListItem={RadioListItem}
data={finalData}
onSelectRow={({value}) => {
setTypeSelected(value);
setLocalTypeSelected(value);
setHasError(false);
}}
shouldSingleExecuteRowSelect
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React, {useCallback, useEffect} from 'react';
import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
Expand Down Expand Up @@ -49,7 +49,7 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb
Navigation.goBack();
};

const goToNextStep = () => {
const goToNextStep = useCallback(() => {
const defaultCardName = getDefaultCardName(assignCard?.cardToAssign?.invitingMemberEmail);
const cardToAssign: Partial<AssignCardData> = {
email: assignCard?.cardToAssign?.invitingMemberEmail,
Expand Down Expand Up @@ -91,7 +91,18 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb
});
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION.getRoute(routeParams), {forceReplace: true});
}
};
}, [
assignCard?.cardToAssign?.invitingMemberEmail,
assignCard?.cardToAssign?.startDate,
assignCard?.cardToAssign?.dateOption,
assignCard?.cardToAssign?.encryptedCardNumber,
assignCard?.cardToAssign?.cardName,
assignCard?.cardToAssign?.customCardName,
filteredCardList,
policyID,
feed,
cardID,
]);

// If the currently inviting member is already a member of the policy then we should just call goToNextStep
// See https://github.com/Expensify/App/issues/74256 for more details
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, {useEffect, useMemo, useState} from 'react';
import ConfirmationPage from '@components/ConfirmationPage';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
// eslint-disable-next-line no-restricted-imports
Expand Down Expand Up @@ -45,8 +45,11 @@ function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageP
const policy = usePolicy(policyID);
const shouldShowTextInput = policy?.employeeList && Object.keys(policy.employeeList).length >= CONST.STANDARD_LIST_ITEM_LIMIT;
const textInputLabel = shouldShowTextInput ? translate('common.search') : undefined;
const workspaceMembers: MemberForList[] = [];
if (policy?.employeeList) {
const workspaceMembers = useMemo((): MemberForList[] => {
const members: MemberForList[] = [];
if (!policy?.employeeList) {
return members;
}
// Get the list of employees from the U4B organization
const uberEmployees = policy?.receiptPartners?.uber?.employees ?? {};

Expand Down Expand Up @@ -85,12 +88,13 @@ function InviteReceiptPartnerPolicyPage({route}: InviteReceiptPartnerPolicyPageP
isSelected: true,
});

workspaceMembers.push(memberForList);
members.push(memberForList);
}
}

sortAlphabetically(workspaceMembers, 'text', localeCompare);
}
sortAlphabetically(members, 'text', localeCompare);
return members;
}, [policy?.employeeList, policy?.receiptPartners?.uber?.employees, isOffline, icons, localeCompare]);

const allMembersWithState: MemberForList[] = [];
if (workspaceMembers.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
isUberConnected,
calculateAndSetThreeDotsMenuPosition,
policy?.receiptPartners?.uber,
policy?.isLoadingReceiptPartners,
isOffline,
startIntegrationFlow,
]);
Expand Down
1 change: 1 addition & 0 deletions src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
policy?.connections?.xero?.config,
policy?.connections?.xero?.data,
policyID,
policyData,
qboConfig?.syncClasses,
qboConfig?.syncCustomers,
qboConfig?.syncLocations,
Expand Down
2 changes: 1 addition & 1 deletion src/types/form/AddDomainMemberForm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ValueOf} from 'type-fest';
import type Form from '@src/types/form/Form';
import type Form from './Form';

const INPUT_IDS = {
EMAIL: 'email',
Expand Down
Loading