From 367fd1f5e25e4f19d8307b468290fe85d4622dc7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 16 Dec 2025 14:39:46 +0100 Subject: [PATCH 1/7] Debounce server search in selectors --- src/pages/InviteReportParticipantsPage.tsx | 27 ++++++------- .../BaseOnboardingWorkspaceInvite.tsx | 25 ++++++------ .../CustomStatus/VacationDelegatePage.tsx | 10 ++--- src/pages/workspace/WorkspaceInvitePage.tsx | 38 ++++++++++++------- .../companyCards/assignCard/AssigneeStep.tsx | 8 ++-- .../expensifyCard/issueNew/AssigneeStep.tsx | 8 ++-- 6 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index 04a105ee916b..36d6fdf14f16 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -58,19 +58,20 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp return res; }, [report]); - const {searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized, onListEndReached} = useSearchSelector({ - selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, - includeUserToInvite: true, - excludeLogins: excludedUsers, - includeRecentReports: true, - shouldInitialize: didScreenTransitionEnd, - }); + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized, onListEndReached} = + useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, + includeUserToInvite: true, + excludeLogins: excludedUsers, + includeRecentReports: true, + shouldInitialize: didScreenTransitionEnd, + }); useEffect(() => { - updateUserSearchPhrase(searchTerm); - searchInServer(searchTerm); - }, [searchTerm]); + updateUserSearchPhrase(debouncedSearchTerm); + searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); const sections = useMemo(() => { const sectionsArray: Sections = []; @@ -148,7 +149,7 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp }, [selectedOptions, goBack, reportID, validate, formatPhoneNumber]); const headerMessage = useMemo(() => { - const processedLogin = searchTerm.trim().toLowerCase(); + const processedLogin = debouncedSearchTerm.trim().toLowerCase(); const expensifyEmails = CONST.EXPENSIFY_EMAILS; if (!availableOptions.userToInvite && expensifyEmails.includes(processedLogin)) { return translate('messages.errorMessageInvalidEmail'); @@ -169,7 +170,7 @@ function InviteReportParticipantsPage({report}: InviteReportParticipantsPageProp false, ); }, [ - searchTerm, + debouncedSearchTerm, availableOptions.userToInvite, availableOptions.recentReports.length, availableOptions.personalDetails.length, diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index a9fa514671a5..b98876a7f7e3 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -67,14 +67,15 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo ); }, [policy?.employeeList]); - const {searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized, searchOptions} = useSearchSelector({ - selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, - includeUserToInvite: true, - excludeLogins: excludedUsers, - includeRecentReports: false, - shouldInitialize: didScreenTransitionEnd, - }); + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized, searchOptions} = + useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, + includeUserToInvite: true, + excludeLogins: excludedUsers, + includeRecentReports: false, + shouldInitialize: didScreenTransitionEnd, + }); const welcomeNoteSubject = useMemo( () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, @@ -84,8 +85,8 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo const welcomeNote = useMemo(() => translate('workspace.common.welcomeNote'), [translate]); useEffect(() => { - searchInServer(searchTerm); - }, [searchTerm]); + searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); const sections: Sections[] = useMemo(() => { const sectionsArr: Sections[] = []; @@ -193,7 +194,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo }, [completeOnboarding, onboardingPolicyID, policy?.employeeList, selectedOptions, welcomeNote, welcomeNoteSubject, formatPhoneNumber]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); if (!availableOptions.userToInvite && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { return translate('messages.errorMessageInvalidEmail'); } @@ -205,7 +206,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo } return getHeaderMessage(searchOptions.personalDetails.length + selectedOptions.length !== 0, !!searchOptions.userToInvite, searchValue, countryCode, false); }, [ - searchTerm, + debouncedSearchTerm, availableOptions.userToInvite, excludedUsers, countryCode, diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx index d542977bd7e7..12f820e96d7a 100644 --- a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -43,7 +43,7 @@ function VacationDelegatePage() { [currentVacationDelegate], ); - const {searchTerm, setSearchTerm, availableOptions, areOptionsInitialized, onListEndReached} = useSearchSelector({ + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, areOptionsInitialized, onListEndReached} = useSearchSelector({ selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, @@ -58,11 +58,11 @@ function VacationDelegatePage() { return getHeaderMessage( (availableOptions.recentReports?.length || 0) + (availableOptions.personalDetails?.length || 0) !== 0, !!availableOptions.userToInvite, - searchTerm.trim(), + debouncedSearchTerm.trim(), countryCode, false, ); - }, [availableOptions.recentReports?.length, availableOptions.personalDetails?.length, availableOptions.userToInvite, searchTerm, countryCode]); + }, [availableOptions.recentReports?.length, availableOptions.personalDetails?.length, availableOptions.userToInvite, debouncedSearchTerm, countryCode]); const sections = useMemo(() => { const sectionsList = []; @@ -159,8 +159,8 @@ function VacationDelegatePage() { ); useEffect(() => { - searchInServer(searchTerm); - }, [searchTerm]); + searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); return ( <> diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 858ef7bafddd..916a5adc8a68 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -94,16 +94,26 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { }); }, [invitedEmailsToAccountIDsDraft, personalDetails]); - const {searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized, onListEndReached, searchOptions} = - useSearchSelector({ - selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, - searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, - includeUserToInvite: true, - excludeLogins: excludedUsers, - includeRecentReports: false, - shouldInitialize: didScreenTransitionEnd, - initialSelected: initiallySelectedOptions, - }); + const { + searchTerm, + debouncedSearchTerm, + setSearchTerm, + availableOptions, + selectedOptions, + selectedOptionsForDisplay, + toggleSelection, + areOptionsInitialized, + onListEndReached, + searchOptions, + } = useSearchSelector({ + selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, + searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE, + includeUserToInvite: true, + excludeLogins: excludedUsers, + includeRecentReports: false, + shouldInitialize: didScreenTransitionEnd, + initialSelected: initiallySelectedOptions, + }); const sections: Sections[] = useMemo(() => { const sectionsArr: Sections[] = []; @@ -179,7 +189,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { ); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); if (!availableOptions.userToInvite && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { return translate('messages.errorMessageInvalidEmail'); } @@ -191,7 +201,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { } return getHeaderMessage(searchOptions.personalDetails.length + selectedOptions.length !== 0, !!searchOptions.userToInvite, searchValue, countryCode, false); }, [ - searchTerm, + debouncedSearchTerm, availableOptions.userToInvite, excludedUsers, countryCode, @@ -218,8 +228,8 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { ); useEffect(() => { - searchInServer(searchTerm); - }, [searchTerm]); + searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); return ( { - searchInServer(searchTerm); - }, [searchTerm]); + searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); if (!availableOptions.userToInvite && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { return translate('messages.errorMessageInvalidEmail'); } return getHeaderMessage(assignees.length > 0, !!availableOptions.userToInvite, searchValue, countryCode, false); - }, [searchTerm, availableOptions.userToInvite, assignees?.length, countryCode, translate]); + }, [debouncedSearchTerm, availableOptions.userToInvite, assignees?.length, countryCode, translate]); const textInputOptions = useMemo( () => ({ diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index cf3e1e6ddd5b..68531f41cdc6 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -187,16 +187,16 @@ function AssigneeStep({policy, stepNames, startStepIndex, route}: AssigneeStepPr ]); useEffect(() => { - searchInServer(searchTerm); - }, [searchTerm]); + searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); if (!availableOptions.userToInvite && CONST.EXPENSIFY_EMAILS_OBJECT[searchValue]) { return translate('messages.errorMessageInvalidEmail'); } return getHeaderMessage(assignees.length > 0, !!availableOptions.userToInvite, searchValue, countryCode, false); - }, [searchTerm, availableOptions.userToInvite, assignees.length, countryCode, translate]); + }, [debouncedSearchTerm, availableOptions.userToInvite, assignees.length, countryCode, translate]); const textInputOptions = useMemo( () => ({ From 3890e65eee55bda0e29520f098562875302da092 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 16 Dec 2025 14:51:21 +0100 Subject: [PATCH 2/7] covering money request participants page --- .../iou/request/MoneyRequestParticipantsSelector.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index d504f03f1edd..c2da2ec6172d 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -213,7 +213,7 @@ function MoneyRequestParticipantsSelector({ [isIOUSplit, iouType, onParticipantsAdded], ); - const {searchTerm, setSearchTerm, availableOptions, selectedOptions, toggleSelection, areOptionsInitialized, onListEndReached, contactState} = useSearchSelector({ + const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedOptions, toggleSelection, areOptionsInitialized, onListEndReached, contactState} = useSearchSelector({ selectionMode: isIOUSplit ? CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI : CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, includeUserToInvite: !isCategorizeOrShareAction && !isPerDiemRequest, @@ -234,11 +234,11 @@ function MoneyRequestParticipantsSelector({ }, }); - const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]); + const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); useEffect(() => { - searchInServer(searchTerm.trim()); - }, [searchTerm]); + searchInServer(debouncedSearchTerm.trim()); + }, [debouncedSearchTerm]); const inputHelperText = useMemo( () => @@ -246,7 +246,7 @@ function MoneyRequestParticipantsSelector({ (availableOptions.personalDetails ?? []).length + (availableOptions.recentReports ?? []).length + (availableOptions.workspaceChats ?? []).length !== 0 || !isEmptyObject(availableOptions.selfDMChat), !!availableOptions?.userToInvite, - searchTerm.trim(), + debouncedSearchTerm.trim(), countryCode, participants.some((participant) => getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), ), @@ -259,7 +259,7 @@ function MoneyRequestParticipantsSelector({ availableOptions?.userToInvite, availableOptions.workspaceChats, cleanSearchTerm, - searchTerm, + debouncedSearchTerm, participants, countryCode, ], From a2d4e250163c31fe2a96e679968bc27b86443c4b Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 16 Dec 2025 15:03:28 +0100 Subject: [PATCH 3/7] fixing lint --- .../settings/Profile/CustomStatus/VacationDelegatePage.tsx | 5 +++-- src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx | 5 +++-- src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx index 12f820e96d7a..8a8a730a7371 100644 --- a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -2,11 +2,11 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionListWithSections'; import UserListItem from '@components/SelectionListWithSections/UserListItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useSearchSelector from '@hooks/useSearchSelector'; @@ -25,6 +25,7 @@ import type {Participant} from '@src/types/onyx/IOU'; function VacationDelegatePage() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [newVacationDelegate, setNewVacationDelegate] = useState(''); const {login: currentUserLogin} = useCurrentUserPersonalDetails(); @@ -82,7 +83,7 @@ function VacationDelegatePage() { shouldShowSubscript: undefined, icons: [ { - source: delegatePersonalDetails?.avatar ?? Expensicons.FallbackAvatar, + source: delegatePersonalDetails?.avatar ?? icons.FallbackAvatar, name: formatPhoneNumber(delegatePersonalDetails?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: delegatePersonalDetails?.accountID, diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 8678dcee5966..cf1c1716baf3 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -1,7 +1,6 @@ import React, {useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; @@ -9,6 +8,7 @@ import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsList from '@hooks/useCardsList'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -46,6 +46,7 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) { const policyID = route.params.policyID; const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); const {isOffline} = useNetwork(); const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: false}); @@ -153,7 +154,7 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) { isSelected: assignCard?.data?.email === email, icons: [ { - source: personalDetail?.avatar ?? Expensicons.FallbackAvatar, + source: personalDetail?.avatar ?? icons.FallbackAvatar, name: formatPhoneNumber(email), type: CONST.ICON_TYPE_AVATAR, id: personalDetail?.accountID, diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index 68531f41cdc6..0c3247b582b6 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -1,12 +1,12 @@ import React, {useEffect, useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useCurrencyForExpensifyCard from '@hooks/useCurrencyForExpensifyCard'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -44,6 +44,7 @@ type AssigneeStepProps = { function AssigneeStep({policy, stepNames, startStepIndex, route}: AssigneeStepProps) { const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); const {isOffline} = useNetwork(); const policyID = route.params.policyID; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); @@ -136,7 +137,7 @@ function AssigneeStep({policy, stepNames, startStepIndex, route}: AssigneeStepPr isSelected: issueNewCard?.data?.assigneeEmail === email, icons: [ { - source: personalDetail?.avatar ?? Expensicons.FallbackAvatar, + source: personalDetail?.avatar ?? icons.FallbackAvatar, name: formatPhoneNumber(email), type: CONST.ICON_TYPE_AVATAR, id: personalDetail?.accountID, From b8cc061a2a79bcd6afe3f5aa54141703a8f18a28 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 16 Dec 2025 15:06:46 +0100 Subject: [PATCH 4/7] minor edit --- .../settings/Profile/CustomStatus/VacationDelegatePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx index 8a8a730a7371..d39cef1d5e24 100644 --- a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -128,7 +128,7 @@ function VacationDelegatePage() { shouldShowSubscript: option.shouldShowSubscript ?? undefined, })), })); - }, [vacationDelegate, delegatePersonalDetails, availableOptions.personalDetails, availableOptions.recentReports, translate, availableOptions.userToInvite]); + }, [vacationDelegate, delegatePersonalDetails, availableOptions.personalDetails, availableOptions.recentReports, translate, availableOptions.userToInvite, icons.FallbackAvatar]); const onSelectRow = useCallback( (option: Participant) => { From d7ceffebb4e42cf77c3614b87e1d8320b004245d Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 16 Dec 2025 15:08:05 +0100 Subject: [PATCH 5/7] eslint fix --- src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx | 2 +- src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index cf1c1716baf3..55cf0c0a5b70 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -166,7 +166,7 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) { membersList = sortAlphabetically(membersList, 'text', localeCompare); return membersList; - }, [isOffline, policy?.employeeList, assignCard?.data?.email, formatPhoneNumber, localeCompare]); + }, [isOffline, policy?.employeeList, assignCard?.data?.email, formatPhoneNumber, localeCompare, icons.FallbackAvatar]); const assignees = useMemo(() => { if (!debouncedSearchTerm) { diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index 0c3247b582b6..ab9099c1ed65 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -149,7 +149,7 @@ function AssigneeStep({policy, stepNames, startStepIndex, route}: AssigneeStepPr membersList = sortAlphabetically(membersList, 'text', localeCompare); return membersList; - }, [policy?.employeeList, localeCompare, isOffline, issueNewCard?.data?.assigneeEmail, formatPhoneNumber]); + }, [policy?.employeeList, localeCompare, isOffline, issueNewCard?.data?.assigneeEmail, formatPhoneNumber, icons.FallbackAvatar]); const assignees = useMemo(() => { if (!debouncedSearchTerm) { From ae5ded78d1dad341c8c94e6445fe9a3bc36bbb7c Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sat, 20 Dec 2025 01:30:46 +0100 Subject: [PATCH 6/7] fixing merge conflicts --- .../settings/Profile/CustomStatus/VacationDelegatePage.tsx | 3 +-- src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx index 217f756402d5..2d09aa9960f7 100644 --- a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -26,7 +26,6 @@ import type {Participant} from '@src/types/onyx/IOU'; function VacationDelegatePage() { const {translate} = useLocalize(); const styles = useThemeStyles(); - const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [newVacationDelegate, setNewVacationDelegate] = useState(''); const {login: currentUserLogin} = useCurrentUserPersonalDetails(); @@ -36,7 +35,7 @@ function VacationDelegatePage() { const [vacationDelegate] = useOnyx(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, {canBeMissing: true}); const currentVacationDelegate = vacationDelegate?.delegate; const delegatePersonalDetails = getPersonalDetailByEmail(currentVacationDelegate ?? ''); - const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); const excludeLogins = useMemo( () => ({ diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 8aa511ef21b4..cff260f63014 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -16,7 +16,7 @@ import useOnyx from '@hooks/useOnyx'; import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftInviteAccountID} from '@libs/actions/Card'; -import {searchInServer} from '@libs/actions/Report'; +import {searchInServer} from '@libs/actions/Reporconst assigneest'; import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -202,6 +202,7 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) { debouncedSearchTerm, membersDetails, selectedOptionsForDisplay, + icons.FallbackAvatar, ]); useEffect(() => { From df3edb40adf20df7b4ccac5d84f11f8234861abe Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sat, 20 Dec 2025 01:44:19 +0100 Subject: [PATCH 7/7] adding tests for useSearchSelectorTest --- .../companyCards/assignCard/AssigneeStep.tsx | 3 +- tests/unit/hooks/useDebouncedState.test.ts | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/unit/hooks/useDebouncedState.test.ts diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index cff260f63014..8aa511ef21b4 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -16,7 +16,7 @@ import useOnyx from '@hooks/useOnyx'; import useSearchSelector from '@hooks/useSearchSelector'; import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftInviteAccountID} from '@libs/actions/Card'; -import {searchInServer} from '@libs/actions/Reporconst assigneest'; +import {searchInServer} from '@libs/actions/Report'; import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -202,7 +202,6 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) { debouncedSearchTerm, membersDetails, selectedOptionsForDisplay, - icons.FallbackAvatar, ]); useEffect(() => { diff --git a/tests/unit/hooks/useDebouncedState.test.ts b/tests/unit/hooks/useDebouncedState.test.ts new file mode 100644 index 000000000000..10709e8d50e2 --- /dev/null +++ b/tests/unit/hooks/useDebouncedState.test.ts @@ -0,0 +1,86 @@ +import {act, renderHook} from '@testing-library/react-native'; +import useDebouncedState from '@hooks/useDebouncedState'; +import CONST from '@src/CONST'; + +describe('useDebouncedState', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('should update immediate value synchronously while debounced value waits', () => { + const {result} = renderHook(() => useDebouncedState('')); + + act(() => { + result.current[2]('john'); + }); + + expect(result.current[0]).toBe('john'); + expect(result.current[1]).toBe(''); + }); + + it('should update debounced value after delay elapses', () => { + const {result} = renderHook(() => useDebouncedState('')); + + act(() => { + result.current[2]('john'); + }); + + act(() => { + jest.advanceTimersByTime(CONST.TIMING.USE_DEBOUNCED_STATE_DELAY); + }); + + expect(result.current[1]).toBe('john'); + }); + + it('should debounce rapid changes and only emit final value', () => { + const {result} = renderHook(() => useDebouncedState('')); + + // Simulate rapid typing + act(() => { + result.current[2]('j'); + }); + act(() => { + jest.advanceTimersByTime(50); + result.current[2]('jo'); + }); + act(() => { + jest.advanceTimersByTime(50); + result.current[2]('joh'); + }); + act(() => { + jest.advanceTimersByTime(50); + result.current[2]('john'); + }); + + // Debounced value should still be empty + expect(result.current[1]).toBe(''); + + act(() => { + jest.advanceTimersByTime(CONST.TIMING.USE_DEBOUNCED_STATE_DELAY); + }); + + // Only final value should be emitted + expect(result.current[1]).toBe('john'); + }); + + it('should cancel pending updates on unmount', () => { + const {result, unmount} = renderHook(() => useDebouncedState('')); + + act(() => { + result.current[2]('pending'); + }); + + unmount(); + + expect(() => { + act(() => { + jest.advanceTimersByTime(CONST.TIMING.USE_DEBOUNCED_STATE_DELAY); + }); + }).not.toThrow(); + }); +});