Skip to content

[CP Staging] fix: search bar performance followup #61844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 13, 2025
Merged
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
19 changes: 11 additions & 8 deletions src/components/SelectionListWithModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ type SelectionListWithModalProps<TItem extends ListItem> = SelectionListProps<TI
onTurnOnSelectionMode?: (item: TItem | null) => void;
isSelected?: (item: TItem) => boolean;
isScreenFocused?: boolean;
selectedItemKeys?: Array<string | number>;
};

function SelectionListWithModal<TItem extends ListItem>(
{turnOnSelectionModeOnLongPress, onTurnOnSelectionMode, onLongPressRow, isScreenFocused = false, sections, isSelected, ...rest}: SelectionListWithModalProps<TItem>,
{turnOnSelectionModeOnLongPress, onTurnOnSelectionMode, onLongPressRow, isScreenFocused = false, sections, isSelected, selectedItemKeys, ...rest}: SelectionListWithModalProps<TItem>,
ref: ForwardedRef<SelectionListHandle>,
) {
const [isModalVisible, setIsModalVisible] = useState(false);
Expand All @@ -40,12 +41,14 @@ function SelectionListWithModal<TItem extends ListItem>(

useEffect(() => {
// We can access 0 index safely as we are not displaying multiple sections in table view
const selectedItems = sections[0].data.filter((item) => {
if (isSelected) {
return isSelected(item);
}
return !!item.isSelected;
});
const selectedItems =
selectedItemKeys ??
sections[0].data.filter((item) => {
if (isSelected) {
return isSelected(item);
}
return !!item.isSelected;
});
selectionRef.current = selectedItems.length;

if (!isSmallScreenWidth) {
Expand All @@ -66,7 +69,7 @@ function SelectionListWithModal<TItem extends ListItem>(
turnOffMobileSelectionMode();
}
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [sections, isSmallScreenWidth, isSelected, isFocused]);
}, [sections, selectedItemKeys, selectionMode, isSmallScreenWidth, isSelected, isFocused]);

useEffect(
() => () => {
Expand Down
17 changes: 12 additions & 5 deletions src/hooks/useSearchResults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useEffect, useState, useTransition} from 'react';
import {useEffect, useRef, useState, useTransition} from 'react';
import type {ListItem} from '@components/SelectionList/types';
import CONST from '@src/CONST';
import usePrevious from './usePrevious';

Expand All @@ -7,15 +8,21 @@ import usePrevious from './usePrevious';
* It utilizes `useTransition` to allow the searchQuery to change rapidly, while more expensive renders that occur using
* the result of the filtering and sorting are deprioritized, allowing them to happen in the background.
*/
function useSearchResults<TValue>(data: TValue[], filterData: (datum: TValue, searchInput: string) => boolean, sortData: (data: TValue[]) => TValue[] = (d) => d) {
function useSearchResults<TValue extends ListItem>(data: TValue[], filterData: (datum: TValue, searchInput: string) => boolean, sortData: (data: TValue[]) => TValue[] = (d) => d) {
const [inputValue, setInputValue] = useState('');
const [result, setResult] = useState(data);
const [, startTransition] = useTransition();
const prevData = usePrevious(data);
const prevInputValueRef = useRef<string | undefined>(undefined);
const [, startTransition] = useTransition();
useEffect(() => {
const normalizedSearchQuery = inputValue.trim().toLowerCase();
const filtered = normalizedSearchQuery.length ? data.filter((item) => filterData(item, normalizedSearchQuery)) : data;
if (prevInputValueRef.current === inputValue) {
setResult(filtered);
return;
}
prevInputValueRef.current = inputValue;
startTransition(() => {
const normalizedSearchQuery = inputValue.trim().toLowerCase();
const filtered = normalizedSearchQuery.length ? data.filter((item) => filterData(item, normalizedSearchQuery)) : data;
const sorted = sortData(filtered);
setResult(sorted);
});
Expand Down
7 changes: 3 additions & 4 deletions src/pages/workspace/WorkspaceMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,6 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson

const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout;

const sections = useMemo(() => [{data: filteredData, isDisabled: false}], [filteredData]);

return (
<WorkspacePageWithSections
headerText={selectionModeHeader ? translate('common.selectMultiple') : translate('workspace.common.members')}
Expand Down Expand Up @@ -774,7 +772,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.flexGrow1, styles.flexShrink0]}
>
{shouldUseNarrowLayout && filteredData.length > 0 && <View style={[styles.pr5]}>{getHeaderContent()}</View>}
{shouldUseNarrowLayout && data.length > 0 && <View style={[styles.pr5]}>{getHeaderContent()}</View>}
{!shouldUseNarrowLayout && (
<>
{!!headerMessage && (
Expand All @@ -797,7 +795,8 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
<SelectionListWithModal
ref={selectionListRef}
canSelectMultiple={canSelectMultiple}
sections={sections}
sections={[{data: filteredData, isDisabled: false}]}
selectedItemKeys={selectedEmployees}
ListItem={TableListItem}
turnOnSelectionModeOnLongPress={isPolicyAdmin}
onTurnOnSelectionMode={(item) => item && toggleUser(item?.accountID)}
Expand Down
7 changes: 3 additions & 4 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
}, []);
const [inputValue, setInputValue, filteredCategoryList] = useSearchResults(categoryList, filterCategory, sortCategories);

useAutoTurnSelectionModeOffWhenHasNoActiveOption(filteredCategoryList);

const sections = useMemo(() => [{data: filteredCategoryList, isDisabled: false}], [filteredCategoryList]);
useAutoTurnSelectionModeOffWhenHasNoActiveOption(categoryList);

const toggleCategory = useCallback(
(category: PolicyOption) => {
Expand Down Expand Up @@ -466,7 +464,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
canSelectMultiple={canSelectMultiple}
turnOnSelectionModeOnLongPress={isSmallScreenWidth}
onTurnOnSelectionMode={(item) => item && toggleCategory(item)}
sections={sections}
sections={[{data: filteredCategoryList, isDisabled: false}]}
selectedItemKeys={selectedCategories}
onCheckboxPress={toggleCategory}
onSelectRow={navigateToCategorySettings}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ function PolicyDistanceRatesPage({
}, []);
const sortRates = useCallback((rates: RateForList[]) => rates.sort((a, b) => (a.rate ?? 0) - (b.rate ?? 0)), []);
const [inputValue, setInputValue, filteredDistanceRatesList] = useSearchResults(distanceRatesList, filterRate, sortRates);
const sections = useMemo(() => [{data: filteredDistanceRatesList, key: 'distanceRatesList'}], [filteredDistanceRatesList]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why should the useMemo not be used anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It causes an extra rerender and caused a bug that I found when implementing #61656.


const addRate = () => {
Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(policyID));
Expand Down Expand Up @@ -411,7 +410,8 @@ function PolicyDistanceRatesPage({
canSelectMultiple={canSelectMultiple}
turnOnSelectionModeOnLongPress
onTurnOnSelectionMode={(item) => item && toggleRate(item)}
sections={sections}
sections={[{data: filteredDistanceRatesList, isDisabled: false}]}
selectedItemKeys={selectedDistanceRates}
onCheckboxPress={toggleRate}
onSelectRow={openRateDetails}
onSelectAll={toggleAllRates}
Expand Down
40 changes: 23 additions & 17 deletions src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,12 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: false});
const {selectionMode} = useMobileSelectionMode();

const customUnit = getPerDiemCustomUnit(policy);
const customUnit = useMemo(() => getPerDiemCustomUnit(policy), [policy]);
const customUnitRates: Record<string, Rate> = useMemo(() => customUnit?.rates ?? {}, [customUnit]);

const allRatesArray = Object.values(customUnitRates);
const allRatesArray = useMemo(() => Object.values(customUnitRates), [customUnitRates]);

const allSubRates = getSubRatesData(allRatesArray);
const allSubRates = useMemo(() => getSubRatesData(allRatesArray), [allRatesArray]);

const canSelectMultiple = shouldUseNarrowLayout ? selectionMode?.isEnabled : true;

Expand Down Expand Up @@ -203,7 +203,6 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
}, []);
const sortRates = useCallback((rates: PolicyOption[]) => lodashSortBy(rates, 'text', localeCompare) as PolicyOption[], []);
const [inputValue, setInputValue, filteredSubRatesList] = useSearchResults(subRatesList, filterRate, sortRates);
const sections = useMemo(() => [{data: filteredSubRatesList, isDisabled: false}], [filteredSubRatesList]);

const toggleSubRate = (subRate: PolicyOption) => {
if (selectedPerDiem.find((selectedSubRate) => selectedSubRate.subRateID === subRate.subRateID) !== undefined) {
Expand All @@ -229,19 +228,25 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
}
};

const getCustomListHeader = () => (
<View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, canSelectMultiple && styles.pl3, !canSelectMultiple && [styles.ph9, styles.pv3, styles.pb5]]}>
<View style={styles.flex3}>
<Text style={[styles.textMicroSupporting, styles.alignSelfStart]}>{translate('common.destination')}</Text>
const getCustomListHeader = () => {
const header = (
<View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, canSelectMultiple && styles.pl3]}>
<View style={styles.flex3}>
<Text style={[styles.textMicroSupporting, styles.alignSelfStart]}>{translate('common.destination')}</Text>
</View>
<View style={styles.flex2}>
<Text style={[styles.textMicroSupporting, styles.alignItemsStart, styles.pl2]}>{translate('common.subrate')}</Text>
</View>
<View style={styles.flex2}>
<Text style={[styles.textMicroSupporting, styles.alignSelfEnd]}>{translate('workspace.perDiem.amount')}</Text>
</View>
</View>
<View style={styles.flex2}>
<Text style={[styles.textMicroSupporting, styles.alignItemsStart, styles.pl2]}>{translate('common.subrate')}</Text>
</View>
<View style={styles.flex2}>
<Text style={[styles.textMicroSupporting, styles.alignSelfEnd]}>{translate('workspace.perDiem.amount')}</Text>
</View>
</View>
);
);
if (canSelectMultiple) {
return header;
}
return <View style={!canSelectMultiple && [styles.ph9, styles.pv3, styles.pb5]}>{header}</View>;
};

const openSettings = () => {
Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_SETTINGS.getRoute(policyID));
Expand Down Expand Up @@ -446,7 +451,8 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
canSelectMultiple={canSelectMultiple}
turnOnSelectionModeOnLongPress
onTurnOnSelectionMode={(item) => item && toggleSubRate(item)}
sections={sections}
sections={[{data: filteredSubRatesList, isDisabled: false}]}
selectedItemKeys={selectedPerDiem.map((item) => item.subRateID)}
onCheckboxPress={toggleSubRate}
onSelectRow={openSubRateDetails}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
Expand Down
Loading