Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e4bafc2
add PERSONAL_AND_WORKSPACE_CARD_LIST derived value
LukasMod Jan 12, 2026
8d50afd
move defaultExpensifyCard to useSearchTypeMenuSections
LukasMod Jan 12, 2026
e62e95a
restore useFilterFormValues
LukasMod Jan 12, 2026
f6cc44d
add isCard guard to filterCardsHiddenFromSearch
LukasMod Jan 12, 2026
1aadaf5
prettier
LukasMod Jan 12, 2026
a292163
move updateAdvancedFilters useEffect to useFilterFormValues
LukasMod Jan 13, 2026
bcde84e
rename to NON_PERSONAL_AND_WORKSPACE_CARD_LIST
LukasMod Jan 14, 2026
9f17e50
consolidate defaultExpensifyCard useMemo
LukasMod Jan 14, 2026
c72f71a
prettier
LukasMod Jan 14, 2026
73ff1e7
revert moving useEffect to useFilterFormValues
LukasMod Jan 14, 2026
e9d6b62
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
LukasMod Jan 16, 2026
d3108e3
add defaultExpensifyCardSelector
LukasMod Jan 16, 2026
e315d20
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
LukasMod Jan 20, 2026
9a626b4
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
LukasMod Jan 20, 2026
0df829e
add useMemo comment in useFilterFormValues
LukasMod Jan 20, 2026
2a08b4e
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
LukasMod Jan 21, 2026
c1b1092
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
LukasMod Jan 23, 2026
8405e4a
update naming
LukasMod Jan 26, 2026
8548a79
add defaultExpensifyCardSelector test
LukasMod Jan 26, 2026
d589ead
Create Card selectors
LukasMod Jan 26, 2026
ef0b684
add nonPersonalAndWorkspaceCardList unit tests
LukasMod Jan 26, 2026
9b68bf9
add Card selector tests
LukasMod Jan 26, 2026
fea489c
prettier and typecheck fix
LukasMod Jan 26, 2026
0620944
fix flaky tests
LukasMod Jan 26, 2026
e694a16
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
LukasMod Jan 27, 2026
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
2 changes: 2 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,7 @@ const ONYXKEYS = {
REPORT_ATTRIBUTES: 'reportAttributes',
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID',
NON_PERSONAL_AND_WORKSPACE_CARD_LIST: 'nonPersonalAndWorkspaceCardList',
CARD_FEED_ERRORS: 'cardFeedErrors',
},

Expand Down Expand Up @@ -1397,6 +1398,7 @@ type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue;
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue;
[ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue;
[ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST]: OnyxTypes.NonPersonalAndWorkspaceCardListDerivedValue;
[ONYXKEYS.DERIVED.CARD_FEED_ERRORS]: OnyxTypes.CardFeedErrorsDerivedValue;
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/EReceipt.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {filterPersonalCards} from '@selectors/Card';
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import useCurrencyList from '@hooks/useCurrencyList';
Expand All @@ -8,7 +9,7 @@ import useOnyx from '@hooks/useOnyx';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {filterPersonalCards, getCardDescription, getCompanyCardDescription} from '@libs/CardUtils';
import {getCardDescription, getCompanyCardDescription} from '@libs/CardUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getTransactionDetails} from '@libs/ReportUtils';
Expand Down
2 changes: 1 addition & 1 deletion src/components/ReportActionItem/IssueCardMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useRoute} from '@react-navigation/native';
import {filterPersonalCards} from '@selectors/Card';
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
Expand All @@ -8,7 +9,6 @@ import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromR
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {filterPersonalCards} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
Expand Down
8 changes: 3 additions & 5 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type {ViolationField} from '@hooks/useViolations';
import useViolations from '@hooks/useViolations';
import {initSplitExpense, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@libs/actions/IOU/index';
import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
import {filterPersonalCards, getCompanyCardDescription, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {getCompanyCardDescription} from '@libs/CardUtils';
import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
Expand Down Expand Up @@ -231,9 +231,7 @@ function MoneyRequestView({
const targetPolicyID = updatedTransaction?.reportID ? parentReport?.policyID : policyID;
const allPolicyTags = usePolicyTags();
const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`];
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
const [companyCardList] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(companyCardList ?? CONST.EMPTY_OBJECT, cardList), [companyCardList, cardList]);
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true});

const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true});
const transactionViolations = useTransactionViolations(transaction?.transactionID);
Expand Down Expand Up @@ -303,7 +301,7 @@ function MoneyRequestView({
const transactionOriginalAmount = transaction && getOriginalAmountForDisplay(transaction, isExpenseReport(moneyRequestReport));
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isManagedCardTransaction = isCardTransactionTransactionUtils(transaction);
const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, allCards);
const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, nonPersonalAndWorkspaceCards);
const shouldShowCard = isManagedCardTransaction && cardProgramName;

const taxRates = policy?.taxRates;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ type SearchAutocompleteListProps = {
allFeeds: Record<string, CardFeeds | undefined> | undefined;

/** All cards */
allCards: CardList;
allCards: CardList | undefined;

/** Reference to the outer element */
ref?: ForwardedRef<SelectionListHandle>;
Expand Down Expand Up @@ -183,7 +183,7 @@ function SearchAutocompleteList({
personalDetails,
reports,
allFeeds,
allCards,
allCards = CONST.EMPTY_OBJECT,
ref,
}: SearchAutocompleteListProps) {
const styles = useThemeStyles();
Expand Down
9 changes: 3 additions & 6 deletions src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceList from '@hooks/useWorkspaceList';
import {close} from '@libs/actions/Modal';
import {handleBulkPayItemSelected, updateAdvancedFilters} from '@libs/actions/Search';
import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -106,9 +105,8 @@ function SearchFiltersBar({
const {currencyList, getCurrencySymbol} = useCurrencyList();

const [email] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector});
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true});
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter', 'Columns']);
Expand Down Expand Up @@ -137,7 +135,6 @@ function SearchFiltersBar({
}));
}, [workspaces]);

const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions ?? {}), [selectedTransactions]);
const hasMultipleOutputCurrency = useMemo(() => {
const policies = Object.values(allPolicies ?? {}).filter((policy): policy is Policy => isPaidGroupPolicy(policy));
Expand Down Expand Up @@ -199,10 +196,10 @@ function SearchFiltersBar({

const [feedOptions, feed] = useMemo(() => {
const feedFilterValues = flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED)?.filters?.map((filter) => filter.value);
const options = getFeedOptions(allFeeds, allCards);
const options = getFeedOptions(allFeeds, nonPersonalAndWorkspaceCards);
const value = feedFilterValues ? options.filter((option) => feedFilterValues.includes(option.value)) : [];
return [options, value];
}, [flatFilters, allFeeds, allCards]);
}, [flatFilters, allFeeds, nonPersonalAndWorkspaceCards]);

const [statusOptions, status] = useMemo(() => {
const options = type ? getStatusOptions(translate, type.value) : [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {navigateToAndOpenReport} from '@libs/actions/Report';
import {setSearchContext} from '@libs/actions/Search';
import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
Expand Down Expand Up @@ -62,13 +61,11 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false});
const taxRates = useMemo(() => getAllTaxRates(policies), [policies]);
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true});
const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const {inputQuery: originalInputQuery} = queryJSON;
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false});
const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, allFeeds, policies, currentUserAccountID, true);
const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, policies, currentUserAccountID, true);

const [searchContext] = useOnyx(ONYXKEYS.SEARCH_CONTEXT, {canBeMissing: true});
const shouldShowQuery = searchContext?.shouldShowSearchQuery ?? false;
Expand Down Expand Up @@ -115,9 +112,9 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
}, [queryText, shouldShowQuery]);

useEffect(() => {
const substitutionsMap = buildSubstitutionsMap(originalInputQuery, personalDetails, reports, taxRates, allCards, allFeeds, policies, currentUserAccountID);
const substitutionsMap = buildSubstitutionsMap(originalInputQuery, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, policies, currentUserAccountID);
setAutocompleteSubstitutions(substitutionsMap);
}, [allFeeds, allCards, originalInputQuery, personalDetails, reports, taxRates, policies, currentUserAccountID]);
}, [allFeeds, nonPersonalAndWorkspaceCards, originalInputQuery, personalDetails, reports, taxRates, policies, currentUserAccountID]);

useEffect(() => {
if (searchRouterListVisible) {
Expand Down Expand Up @@ -400,7 +397,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
ref={listRef}
personalDetails={personalDetails}
reports={reports}
allCards={allCards}
allCards={nonPersonalAndWorkspaceCards}
allFeeds={allFeeds}
textInputRef={textInputRef}
/>
Expand Down Expand Up @@ -475,7 +472,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
shouldSubscribeToArrowKeyEvents={isAutocompleteListVisible}
personalDetails={personalDetails}
reports={reports}
allCards={allCards}
allCards={nonPersonalAndWorkspaceCards}
allFeeds={allFeeds}
textInputRef={textInputRef}
/>
Expand Down
9 changes: 3 additions & 6 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {deepEqual} from 'fast-equals';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {TextInputProps} from 'react-native';
import {InteractionManager, View} from 'react-native';
import type {ValueOf} from 'type-fest';
Expand All @@ -25,7 +25,6 @@ import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useRootNavigationState from '@hooks/useRootNavigationState';
import useThemeStyles from '@hooks/useThemeStyles';
import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {scrollToRight} from '@libs/InputUtils';
import Log from '@libs/Log';
import backHistory from '@libs/Navigation/helpers/backHistory';
Expand Down Expand Up @@ -73,9 +72,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true});
const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const {shouldUseNarrowLayout} = useResponsiveLayout();
const listRef = useRef<SelectionListHandle>(null);
Expand Down Expand Up @@ -526,7 +523,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
personalDetails={personalDetails}
reports={reports}
allFeeds={allFeeds}
allCards={allCards}
allCards={nonPersonalAndWorkspaceCards}
/>
)}
{!shouldShowList && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function buildSubstitutionsMap(
personalDetails: PersonalDetailsList | undefined,
reports: OnyxCollection<Report>,
allTaxRates: Record<string, string[]>,
cardList: CardList,
cardList: CardList | undefined,
cardFeeds: OnyxCollection<CardFeeds>,
policies: OnyxCollection<Policy>,
currentUserAccountID: number,
Expand Down
10 changes: 3 additions & 7 deletions src/hooks/useAdvancedSearchFilters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {filterCardsHiddenFromSearch} from '@selectors/Card';
import {emailSelector} from '@selectors/Session';
import {useMemo} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {getAllTaxRates, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils';
import {getAllPolicyValues} from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -207,10 +206,7 @@ function useAdvancedSearchFilters() {
const {localeCompare} = useLocalize();
const [searchAdvancedFilters = getEmptyObject<SearchAdvancedFiltersForm>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true});
const policyID = searchAdvancedFilters.policyID;
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: false});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: false});
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList, true), [userCardList, workspaceCardFeeds]);

const [searchCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch});
const [policies = getEmptyObject<NonNullable<OnyxCollection<Policy>>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false});
const [allPolicyCategories = getEmptyObject<NonNullable<OnyxCollection<PolicyCategories>>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {
canBeMissing: false,
Expand Down Expand Up @@ -253,7 +249,7 @@ function useAdvancedSearchFilters() {
const shouldDisplayAttendeeFilter = isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.IS_ATTENDEE_TRACKING_ENABLED);
const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, selectedPolicyCategories?.length > 0);
const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!selectedPolicyTagLists);
const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(allCards).length, areCardsEnabled);
const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(searchCards ?? {}).length, areCardsEnabled);
const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled);
const shouldDisplayWorkspaceFilter = workspaces.some((section) => section.data.length > 1);
const shouldDisplayGroupCurrencyFilter = !!searchAdvancedFilters.groupBy;
Expand Down
13 changes: 3 additions & 10 deletions src/hooks/useCardFeedsForDisplay.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type {OnyxCollection} from 'react-native-onyx';
import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils';
import {filterPersonalCards, isCustomFeed, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils';
import {isCustomFeed} from '@libs/CardUtils';
import {isPaidGroupPolicy} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {CompanyCardFeed, Policy} from '@src/types/onyx';
import useLocalize from './useLocalize';
Expand Down Expand Up @@ -59,13 +58,7 @@ const useCardFeedsForDisplay = () => {
}
}

const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList);
const expensifyCards = getCardFeedsForDisplay({}, allCards);
const defaultExpensifyCard = Object.values(expensifyCards)?.at(0);

return {defaultCardFeed, cardFeedsByPolicy, defaultExpensifyCard};
return {defaultCardFeed, cardFeedsByPolicy};
};

export default useCardFeedsForDisplay;
6 changes: 4 additions & 2 deletions src/hooks/useFilterFormValues.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useMemo} from 'react';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import type {SearchQueryJSON} from '@components/Search/types';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
Expand All @@ -21,8 +22,9 @@ const useFilterFormValues = (queryJSON?: SearchQueryJSON) => {
const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});

const taxRates = getAllTaxRates(policies);
const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList);
// Helps to avoid unnecessary recalculations when user open report details screen. React Compiler does not provide same result.
const taxRates = useMemo(() => getAllTaxRates(policies), [policies]);
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [workspaceCardFeeds, userCardList]);
Comment on lines +26 to +27

@situchan situchan Jan 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are you sure that useMemo really improves performance?
Not sure why React compiler doesn't handle it already.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did few tests with it, from compiler side it looks like that:
entry code:

const taxRatesMemo = useMemo(() => getAllTaxRates(policies), [policies]); 
const taxRates = getAllTaxRates(policies);

result:

if (
  $[7] !== allReports ||
  $[8] !== personalDetails ||
  $[9] !== policies ||
  $[10] !== policyCategories ||
  $[11] !== policyTagsLists ||
  $[12] !== queryJSON ||
  $[13] !== t7 ||
  $[14] !== userCardList ||
  $[15] !== workspaceCardFeeds
) {
  const currencyList = t7 === undefined ? getEmptyObject() : t7;

  // Memoized version (compiled from useMemo([policies]))
  let t9;
  if ($[20] !== policies) {
    t9 = getAllTaxRates(policies);
    $[20] = policies;
    $[21] = t9;
  } else {
    t9 = $[21];
  }
  taxRatesMemo = t9;

  // Non-memoized version (recomputed on every region re-run)
  taxRates = getAllTaxRates(policies);
}

taxRates recomputes every time something in the region is outdated, but taxRatesMemo recomputes only when additionally policies are outdated, which makes that difference

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I also added some logs inside mergeCardListWithWorkspaceFeeds to compare how many times it fires from that hook and there is a difference (same cause)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@chrispader do you have any idea?

@LukasMod LukasMod Jan 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I checked with logs again and
using just

    const taxRates = getAllTaxRates(policies);
    const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList);

generated 2x of each execution, when opening report. With useMemo its 0. Here is source output with and without memo:

useFIlterFormValues - NO MEMO.txt
useFIlterFormValues - MEMO.txt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Interesting, not sure why React Compiler seems to not memoize most ideally in this case. Maybe this is a bug or React Compiler just decided, that the function is cheap to execute and therefore skips optimization.

@LukasMod do you have a working repro for this so we could investigate this further and potentially create an upstream issue?

also cc @roryabraham

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@chrispader You can just take useFilterFormValues and remove that useMemo and check how source files looks like. From what I understand from output, compiler creates area for returned value formValues and its all dependencies.

For useMemo compiler emits three separate memoized regions:

  1. taxRates memoized on [policies]
  2. allCards memoized on [(workspaceCardFeeds ?? CONST.EMPTY_OBJECT), userCardList]
  3. formValues memoized on [allCards, allReports, currencyList, personalDetails, policyCategories, policyTagsLists, queryJSON, taxRates]

So each derived value can be reused independently.

Without useMemo compiler emits one single memoized region around the entire derivation:

  1. formValues memoized on [allReports, currencyList, personalDetails, policies, policyCategories, policyTagsLists, queryJSON, userCardList, workspaceCardFeeds]

Inside that one region it recomputes taxRates and allCards together, whenever any dependency changes.

Updated files from latest main, you can see changes in region condition:
image
useFIlterFormValues.-.MEMO.txt
useFIlterFormValues.-.NO.MEMO.txt

useFilterFormValues for testing (just check with useMemo and without):

import {useMemo} from 'react';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import type {SearchQueryJSON} from '@components/Search/types';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {getAllTaxRates} from '@libs/PolicyUtils';
import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
import useCurrencyList from './useCurrencyList';
import useOnyx from './useOnyx';

const useFilterFormValues = (queryJSON?: SearchQueryJSON) => {
    const personalDetails = usePersonalDetails();
    const {currencyList} = useCurrencyList();

    const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true});
    const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
    const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
    const [policyTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: true});
    const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: true});
    const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});

    // Helps to avoid unnecessary recalculations when user open report details screen. React Compiler does not provide same result.
    const taxRates = useMemo(() => getAllTaxRates(policies, 'useMemo'), [policies]);
    const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList, false, 'useMemo'), [workspaceCardFeeds, userCardList]);

    // const taxRates =  getAllTaxRates(policies, 'compiler')
    // const allCards =  mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList, false, 'compiler');

    const formValues = queryJSON
        ? buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, allReports, taxRates)
        : getEmptyObject<Partial<SearchAdvancedFiltersForm>>();

    return formValues;
};

export default useFilterFormValues;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any luck looking into this @chrispader ?


const formValues = queryJSON
? buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, allReports, taxRates)
Expand Down
Loading
Loading