Skip to content

Commit a42c40c

Browse files
authored
Merge pull request #88316 from software-mansion-labs/perf/cache-and-defer-heavy-saved-search-computation
perf: Cache and defer heavy saved search computation
2 parents 00f0667 + 04a2135 commit a42c40c

4 files changed

Lines changed: 184 additions & 87 deletions

File tree

src/libs/SearchQueryUtils.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,21 @@ function formatDefaultRawFilterSegment(rawFilter: RawQueryFilter, policies: Onyx
16031603
* We try to replace every numeric id value with a display version of this value,
16041604
* So: user IDs get turned into emails, report ids into report names etc.
16051605
*/
1606+
type BuildUserReadableQueryStringParams = {
1607+
queryJSON: SearchQueryJSON;
1608+
PersonalDetails: OnyxTypes.PersonalDetailsList | undefined;
1609+
reports: OnyxCollection<OnyxTypes.Report>;
1610+
taxRates: Record<string, string[]>;
1611+
cardList: OnyxTypes.CardList | undefined;
1612+
cardFeeds: OnyxCollection<OnyxTypes.CardFeeds>;
1613+
policies: OnyxCollection<OnyxTypes.Policy>;
1614+
currentUserAccountID: number;
1615+
autoCompleteWithSpace: boolean;
1616+
translate: LocalizedTranslate;
1617+
feedKeysWithCards?: FeedKeysWithAssignedCards;
1618+
reportAttributes: OnyxTypes.ReportAttributesDerivedValue['reports'] | undefined;
1619+
};
1620+
16061621
function buildUserReadableQueryString({
16071622
queryJSON,
16081623
PersonalDetails,
@@ -1616,20 +1631,7 @@ function buildUserReadableQueryString({
16161631
translate,
16171632
feedKeysWithCards,
16181633
reportAttributes,
1619-
}: {
1620-
queryJSON: SearchQueryJSON;
1621-
PersonalDetails: OnyxTypes.PersonalDetailsList | undefined;
1622-
reports: OnyxCollection<OnyxTypes.Report>;
1623-
taxRates: Record<string, string[]>;
1624-
cardList: OnyxTypes.CardList | undefined;
1625-
cardFeeds: OnyxCollection<OnyxTypes.CardFeeds>;
1626-
policies: OnyxCollection<OnyxTypes.Policy>;
1627-
currentUserAccountID: number;
1628-
autoCompleteWithSpace: boolean;
1629-
translate: LocalizedTranslate;
1630-
feedKeysWithCards?: FeedKeysWithAssignedCards;
1631-
reportAttributes: OnyxTypes.ReportAttributesDerivedValue['reports'] | undefined;
1632-
}) {
1634+
}: BuildUserReadableQueryStringParams) {
16331635
const {type, status, groupBy, view, columns, policyID, rawFilterList, flatFilters: filters = [], limit} = queryJSON;
16341636

16351637
if (rawFilterList && rawFilterList.length > 0) {
@@ -2145,4 +2147,6 @@ export {
21452147
isAmountFilterKey,
21462148
};
21472149

2150+
export type {BuildUserReadableQueryStringParams};
2151+
21482152
export type {SearchDateValues};

src/pages/Search/SavedSearchList.tsx

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import React from 'react';
44
import MenuItemList from '@components/MenuItemList';
55
import {usePersonalDetails} from '@components/OnyxListItemProvider';
66
import {useProductTrainingContext} from '@components/ProductTrainingContext';
7-
import type {SearchQueryJSON} from '@components/Search/types';
87
import useDeleteSavedSearch from '@hooks/useDeleteSavedSearch';
98
import useFeedKeysWithAssignedCards from '@hooks/useFeedKeysWithAssignedCards';
109
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -17,20 +16,79 @@ import {setSearchContext} from '@libs/actions/Search';
1716
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
1817
import Navigation from '@libs/Navigation/Navigation';
1918
import {getAllTaxRates} from '@libs/PolicyUtils';
20-
import {buildSearchQueryJSON, buildUserReadableQueryString} from '@libs/SearchQueryUtils';
2119
import type {SavedSearchMenuItem} from '@libs/SearchUIUtils';
2220
import {createBaseSavedSearchMenuItem, getOverflowMenu as getOverflowMenuUtil} from '@libs/SearchUIUtils';
2321
import variables from '@styles/variables';
2422
import CONST from '@src/CONST';
2523
import ONYXKEYS from '@src/ONYXKEYS';
2624
import ROUTES from '@src/ROUTES';
2725
import type {SaveSearchItem} from '@src/types/onyx/SaveSearch';
26+
import useSavedSearchTitles from './hooks/useSavedSearchTitles';
2827
import SavedSearchItemThreeDotMenu from './SavedSearchItemThreeDotMenu';
2928

3029
type SavedSearchListProps = {
3130
hash: number | undefined;
3231
};
3332

33+
type SavedSearchMenuItemBuilderParams = {
34+
item: SaveSearchItem;
35+
key: string;
36+
index: number;
37+
hash: number | undefined;
38+
title: string;
39+
getOverflowMenu: (itemName: string, itemHash: number, itemQuery: string) => ReturnType<typeof getOverflowMenuUtil>;
40+
shouldShowSavedSearchTooltip: boolean;
41+
hideSavedSearchTooltip: (() => void) | undefined;
42+
renderSavedSearchTooltip: () => React.JSX.Element;
43+
itemStyle: SavedSearchMenuItem['style'];
44+
tooltipWrapperStyle: SavedSearchMenuItem['tooltipWrapperStyle'];
45+
};
46+
47+
function buildSavedSearchMenuItem({
48+
item,
49+
key,
50+
index,
51+
hash,
52+
title,
53+
getOverflowMenu,
54+
shouldShowSavedSearchTooltip,
55+
hideSavedSearchTooltip,
56+
renderSavedSearchTooltip,
57+
itemStyle,
58+
tooltipWrapperStyle,
59+
}: SavedSearchMenuItemBuilderParams): SavedSearchMenuItem {
60+
const isItemFocused = Number(key) === hash;
61+
const baseMenuItem: SavedSearchMenuItem = createBaseSavedSearchMenuItem(item, key, index, title, isItemFocused);
62+
63+
return {
64+
...baseMenuItem,
65+
role: CONST.ROLE.TAB,
66+
sentryLabel: CONST.SENTRY_LABEL.SEARCH.SAVED_SEARCH_MENU_ITEM,
67+
onPress: () => {
68+
setSearchContext(false);
69+
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: item?.query ?? '', name: item?.name}));
70+
},
71+
rightComponent: (
72+
<SavedSearchItemThreeDotMenu
73+
menuItems={getOverflowMenu(title, Number(key), item.query)}
74+
isDisabledItem={item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}
75+
hideProductTrainingTooltip={index === 0 && shouldShowSavedSearchTooltip ? hideSavedSearchTooltip : undefined}
76+
shouldRenderTooltip={index === 0 && shouldShowSavedSearchTooltip}
77+
renderTooltipContent={renderSavedSearchTooltip}
78+
/>
79+
),
80+
style: itemStyle,
81+
tooltipAnchorAlignment: {
82+
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
83+
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
84+
},
85+
tooltipShiftHorizontal: variables.savedSearchShiftHorizontal,
86+
tooltipShiftVertical: variables.savedSearchShiftVertical,
87+
tooltipWrapperStyle,
88+
renderTooltipContent: renderSavedSearchTooltip,
89+
};
90+
}
91+
3492
function SavedSearchList({hash}: SavedSearchListProps) {
3593
const styles = useThemeStyles();
3694
const {translate} = useLocalize();
@@ -60,61 +118,42 @@ function SavedSearchList({hash}: SavedSearchListProps) {
60118
const taxRates = getAllTaxRates(allPolicies);
61119
const cardsForSavedSearchDisplay = mergeCardListWithWorkspaceFeeds(workspaceCardList ?? CONST.EMPTY_OBJECT, cardList);
62120

63-
const getOverflowMenu = (itemName: string, itemHash: number, itemQuery: string) => getOverflowMenuUtil(expensifyIcons, itemName, itemHash, itemQuery, translate, showDeleteModal);
64-
65-
const createSavedSearchMenuItem = (item: SaveSearchItem, key: string, index: number) => {
66-
let title = item.name;
67-
if (title === item.query) {
68-
const jsonQuery = buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON);
69-
title = buildUserReadableQueryString({
70-
queryJSON: jsonQuery,
71-
PersonalDetails: personalDetails,
72-
reports,
73-
taxRates,
74-
cardList: cardsForSavedSearchDisplay,
75-
cardFeeds: allFeeds,
76-
policies: allPolicies,
77-
currentUserAccountID,
78-
autoCompleteWithSpace: false,
79-
translate,
80-
feedKeysWithCards,
81-
reportAttributes,
82-
});
83-
}
121+
const savedSearchTitles = useSavedSearchTitles({
122+
savedSearches,
123+
PersonalDetails: personalDetails,
124+
reports,
125+
taxRates,
126+
cardList: cardsForSavedSearchDisplay,
127+
cardFeeds: allFeeds,
128+
policies: allPolicies,
129+
currentUserAccountID,
130+
translate,
131+
feedKeysWithCards,
132+
reportAttributes,
133+
});
84134

85-
const isItemFocused = Number(key) === hash;
86-
const baseMenuItem: SavedSearchMenuItem = createBaseSavedSearchMenuItem(item, key, index, title, isItemFocused);
135+
const getOverflowMenu = (itemName: string, itemHash: number, itemQuery: string) => getOverflowMenuUtil(expensifyIcons, itemName, itemHash, itemQuery, translate, showDeleteModal);
87136

88-
return {
89-
...baseMenuItem,
90-
role: CONST.ROLE.TAB,
91-
sentryLabel: CONST.SENTRY_LABEL.SEARCH.SAVED_SEARCH_MENU_ITEM,
92-
onPress: () => {
93-
setSearchContext(false);
94-
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: item?.query ?? '', name: item?.name}));
95-
},
96-
rightComponent: (
97-
<SavedSearchItemThreeDotMenu
98-
menuItems={getOverflowMenu(title, Number(key), item.query)}
99-
isDisabledItem={item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}
100-
hideProductTrainingTooltip={index === 0 && shouldShowSavedSearchTooltip ? hideSavedSearchTooltip : undefined}
101-
shouldRenderTooltip={index === 0 && shouldShowSavedSearchTooltip}
102-
renderTooltipContent={renderSavedSearchTooltip}
103-
/>
104-
),
105-
style: [styles.alignItemsCenter],
106-
tooltipAnchorAlignment: {
107-
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
108-
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
109-
},
110-
tooltipShiftHorizontal: variables.savedSearchShiftHorizontal,
111-
tooltipShiftVertical: variables.savedSearchShiftVertical,
112-
tooltipWrapperStyle: [styles.mh4, styles.pv2, styles.productTrainingTooltipWrapper],
113-
renderTooltipContent: renderSavedSearchTooltip,
114-
};
115-
};
137+
const itemStyle = [styles.alignItemsCenter];
138+
const tooltipWrapperStyle = [styles.mh4, styles.pv2, styles.productTrainingTooltipWrapper];
116139

117-
const savedSearchesMenuItems = savedSearches ? Object.entries(savedSearches).map(([key, item], index) => createSavedSearchMenuItem(item, key, index)) : [];
140+
const savedSearchesMenuItems = savedSearches
141+
? Object.entries(savedSearches).map(([key, item], index) =>
142+
buildSavedSearchMenuItem({
143+
item,
144+
key,
145+
index,
146+
hash,
147+
title: item.name === item.query ? (savedSearchTitles.get(item.query) ?? item.name) : item.name,
148+
getOverflowMenu,
149+
shouldShowSavedSearchTooltip,
150+
hideSavedSearchTooltip,
151+
renderSavedSearchTooltip,
152+
itemStyle,
153+
tooltipWrapperStyle,
154+
}),
155+
)
156+
: [];
118157

119158
return (
120159
<MenuItemList

src/pages/Search/SearchTypeMenuNarrow.tsx

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ import useThemeStyles from '@hooks/useThemeStyles';
2424
import {setSearchContext} from '@libs/actions/Search';
2525
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
2626
import {getAllTaxRates} from '@libs/PolicyUtils';
27-
import {buildSearchQueryJSON, buildUserReadableQueryString} from '@libs/SearchQueryUtils';
2827
import {getItemBadgeText, getOverflowMenu} from '@libs/SearchUIUtils';
2928
import CONST from '@src/CONST';
3029
import ONYXKEYS from '@src/ONYXKEYS';
3130
import {accountIDSelector} from '@src/selectors/Session';
3231
import todosReportCountsSelector from '@src/selectors/Todos';
32+
import useSavedSearchTitles from './hooks/useSavedSearchTitles';
3333

3434
type SearchTypeMenuNarrowProps = {
3535
/** Search query JSON */
@@ -91,6 +91,20 @@ function SearchTypeMenuNarrow({queryJSON, onTabPress}: SearchTypeMenuNarrowProps
9191

9292
const taxRates = getAllTaxRates(allPolicies);
9393
const cardsForSavedSearchDisplay = mergeCardListWithWorkspaceFeeds(workspaceCardList ?? CONST.EMPTY_OBJECT, cardList);
94+
const savedSearchTitles = useSavedSearchTitles({
95+
savedSearches,
96+
PersonalDetails: personalDetails,
97+
reports,
98+
taxRates,
99+
cardList: cardsForSavedSearchDisplay,
100+
cardFeeds: allFeeds,
101+
policies: allPolicies,
102+
currentUserAccountID,
103+
translate,
104+
feedKeysWithCards,
105+
reportAttributes,
106+
enabled: !!queryJSON,
107+
});
94108

95109
const [savedSearchToModifyKey, setSavedSearchToModifyKey] = useState<string | null>(null);
96110
const menuAnchorRef = useRef<View>(null);
@@ -127,24 +141,7 @@ function SearchTypeMenuNarrow({queryJSON, onTabPress}: SearchTypeMenuNarrowProps
127141
return null;
128142
}
129143

130-
let title = item.name;
131-
const itemJsonQuery = buildSearchQueryJSON(item.query);
132-
if (queryJSON && itemJsonQuery && title === item.query) {
133-
title = buildUserReadableQueryString({
134-
queryJSON: itemJsonQuery,
135-
PersonalDetails: personalDetails,
136-
reports,
137-
taxRates,
138-
cardList: cardsForSavedSearchDisplay,
139-
cardFeeds: allFeeds,
140-
policies: allPolicies,
141-
currentUserAccountID,
142-
autoCompleteWithSpace: false,
143-
translate,
144-
feedKeysWithCards,
145-
reportAttributes,
146-
});
147-
}
144+
const title = item.name === item.query ? (savedSearchTitles.get(item.query) ?? item.name) : item.name;
148145

149146
queryMap.set(key, {query: item.query ?? '', name: item.name});
150147
savedSearchesPopoverMenuItems[key] = getOverflowMenu(expensifyIcons, title, Number(key), item.query, translate, showDeleteModal, true, () =>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {useDeferredValue} from 'react';
2+
import {buildSearchQueryJSON, buildUserReadableQueryString} from '@libs/SearchQueryUtils';
3+
import type {BuildUserReadableQueryStringParams} from '@libs/SearchQueryUtils';
4+
import type {SaveSearchItem} from '@src/types/onyx/SaveSearch';
5+
6+
type SavedSearchCollection = Record<string, SaveSearchItem>;
7+
8+
type SavedSearchTitlesHookParams = Omit<BuildUserReadableQueryStringParams, 'queryJSON' | 'autoCompleteWithSpace'> & {
9+
savedSearches: SavedSearchCollection | undefined;
10+
enabled?: boolean;
11+
};
12+
13+
/** Returns a map of raw query string → human-readable title for saved searches that have no custom name. Heavy resolution is deferred to avoid blocking the UI. */
14+
function useSavedSearchTitles({savedSearches, translate, enabled = true, ...rest}: SavedSearchTitlesHookParams): Map<string, string> {
15+
// `savedSearches` and `translate` are intentionally excluded from the deferred object.
16+
// `savedSearches` drives which items appear in the list — deferring it would cause a flash
17+
// `translate` is a stable function reference that never triggers heavy re-computation on its own.
18+
const deferredRest = useDeferredValue(rest);
19+
const {PersonalDetails, reports, taxRates, cardList, cardFeeds, policies, currentUserAccountID, feedKeysWithCards, reportAttributes} = deferredRest;
20+
21+
const titles = new Map<string, string>();
22+
23+
if (!savedSearches || !enabled) {
24+
return titles;
25+
}
26+
27+
for (const item of Object.values(savedSearches)) {
28+
if (item.name !== item.query || titles.has(item.query)) {
29+
continue;
30+
}
31+
32+
const itemJsonQuery = buildSearchQueryJSON(item.query);
33+
if (!itemJsonQuery) {
34+
continue;
35+
}
36+
37+
const title = buildUserReadableQueryString({
38+
queryJSON: itemJsonQuery,
39+
PersonalDetails,
40+
reports,
41+
taxRates,
42+
cardList,
43+
cardFeeds,
44+
policies,
45+
currentUserAccountID,
46+
autoCompleteWithSpace: false,
47+
translate,
48+
feedKeysWithCards,
49+
reportAttributes,
50+
});
51+
titles.set(item.query, title);
52+
}
53+
54+
return titles;
55+
}
56+
57+
export default useSavedSearchTitles;

0 commit comments

Comments
 (0)