Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b71c90e
perf: reduce perceived latency navigating to search after expense cre…
JakubKorytko Mar 17, 2026
5a7f5cc
defer submit expense API write
JakubKorytko Mar 18, 2026
103c16c
delete DeferredSearch component
JakubKorytko Mar 18, 2026
c5dca7f
add comment to SAFETY_TIMEOUT_MS
JakubKorytko Mar 18, 2026
a0d5e89
add gate to API deferral
JakubKorytko Mar 18, 2026
131594a
fix: IOUTest
JakubKorytko Mar 18, 2026
bf6257d
fix: flush deferred search write on re-focus and error/empty bail-out
JakubKorytko Mar 18, 2026
c37bc29
Merge branch 'main' into korytko/perf/optimize-mobile-navigate-to-search
JakubKorytko Mar 18, 2026
ebea71e
add search-level skeleton back
JakubKorytko Mar 18, 2026
c0f704b
Merge branch 'main' into korytko/perf/optimize-mobile-navigate-to-search
JakubKorytko Mar 23, 2026
8a6c1f9
generalize API write delay
JakubKorytko Mar 20, 2026
4b9295f
address luacmartins comments
JakubKorytko Mar 20, 2026
faa40a2
update cspell.json
JakubKorytko Mar 23, 2026
7436542
always display skeleton on submit expense path
JakubKorytko Mar 23, 2026
b18d679
track onyx optimistic update
JakubKorytko Mar 23, 2026
dd7e161
handle optimistic row in Search
JakubKorytko Mar 23, 2026
d552c74
some code improvements
JakubKorytko Mar 23, 2026
c9b9868
fix failing eslint
JakubKorytko Mar 23, 2026
68a86e8
fix invoice span
JakubKorytko Mar 24, 2026
aeec609
defer invoices
JakubKorytko Mar 24, 2026
06b801a
defer per-diem & self-dm's
JakubKorytko Mar 24, 2026
6ee930a
correct navigateToSearch attributes
JakubKorytko Mar 24, 2026
e56416a
Merge branch 'main' into korytko/perf/optimize-mobile-navigate-to-search
JakubKorytko Mar 24, 2026
366b723
fix: requestMoney guard
JakubKorytko Mar 24, 2026
65b791d
flush deferred write based on app state
JakubKorytko Mar 25, 2026
02f1d65
Merge branch 'main' into korytko/perf/optimize-mobile-navigate-to-search
JakubKorytko Mar 25, 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
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@
"Uncapitalize",
"uncategorized",
"Undelete",
"unflushed",
"unheld",
"unhold",
"uninstallations",
Expand Down
4 changes: 4 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,9 @@ const CONST = {
ACTIVITY_INDICATOR_TIMEOUT: 10000,
MIN_SMOOTH_SCROLL_EVENT_THROTTLE: 16,
},
DEFERRED_LAYOUT_WRITE_KEYS: {
SEARCH: 'search',
},
TELEMETRY: {
CONTEXT_FULLSTORY: 'Fullstory',
CONTEXT_MEMORY: 'Memory',
Expand Down Expand Up @@ -1951,6 +1954,7 @@ const CONST = {
ATTRIBUTE_MIN_DURATION: 'min_duration',
ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually',
ATTRIBUTE_IS_WARM: 'is_warm',
ATTRIBUTE_WAS_LIST_EMPTY: 'was_list_empty',
ATTRIBUTE_SKELETON_PREFIX: 'skeleton.',
ATTRIBUTE_SCENARIO: 'scenario',
ATTRIBUTE_HAS_RECEIPT: 'has_receipt',
Expand Down
52 changes: 0 additions & 52 deletions src/components/Search/DeferredSearch/index.native.tsx

This file was deleted.

21 changes: 0 additions & 21 deletions src/components/Search/DeferredSearch/index.tsx

This file was deleted.

196 changes: 112 additions & 84 deletions src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useNavigationState} from '@react-navigation/native';
import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
// We need direct access to useOnyx from react-native-onyx to avoid circular dependencies in SearchContext
// eslint-disable-next-line no-restricted-imports
import {useOnyx} from 'react-native-onyx';
Expand All @@ -8,8 +8,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import usePreviousDefined from '@hooks/usePreviousDefined';
import useTodos from '@hooks/useTodos';
import {getDeepestFocusedScreen} from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
import {isMoneyRequestReport} from '@libs/ReportUtils';
import {buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
import type {SearchKey, SearchTypeMenuItem} from '@libs/SearchUIUtils';
Expand Down Expand Up @@ -81,22 +79,21 @@ const defaultSearchActionsContext: SearchActionsContextValue = {
const SearchStateContext = React.createContext<SearchStateContextValue>(defaultSearchStateContext);
const SearchActionsContext = React.createContext<SearchActionsContextValue>(defaultSearchActionsContext);

function SearchContextProvider({children}: SearchContextProps) {
const focusedScreen = useNavigationState((state) => getDeepestFocusedScreen(state));
const focusedScreenName = focusedScreen?.name;
const focusedScreenParams = focusedScreen?.params;

// Get the params for the search page so that we can derive the search query JSON from it
const params = useMemo(() => {
if (focusedScreenName !== SCREENS.SEARCH.ROOT) {
return undefined;
}
function selectSearchQueryParam(state: Parameters<Parameters<typeof useNavigationState>[0]>[0]) {
const focused = getDeepestFocusedScreen(state);
return focused?.name === SCREENS.SEARCH.ROOT ? (focused.params?.q as string | undefined) : undefined;
}

return focusedScreenParams as PlatformStackScreenProps<SearchFullscreenNavigatorParamList, typeof SCREENS.SEARCH.ROOT>['route']['params'];
}, [focusedScreenName, focusedScreenParams]);
function selectSearchRawQueryParam(state: Parameters<Parameters<typeof useNavigationState>[0]>[0]) {
const focused = getDeepestFocusedScreen(state);
return focused?.name === SCREENS.SEARCH.ROOT ? (focused.params?.rawQuery as string | undefined) : undefined;
}

const queryParam = params?.q;
const rawQueryParam = params?.rawQuery;
function SearchContextProvider({children}: SearchContextProps) {
// Extract only the primitive values we need from the focused screen to avoid
// re-renders from new object references returned by getDeepestFocusedScreen.
const queryParam = useNavigationState(selectSearchQueryParam);
const rawQueryParam = useNavigationState(selectSearchRawQueryParam);
const definedQueryParam = usePreviousDefined(queryParam) ?? buildSearchQueryString();
const currentSearchQueryJSON = useMemo(() => buildSearchQueryJSON(definedQueryParam, rawQueryParam), [definedQueryParam, rawQueryParam]);

Expand All @@ -107,9 +104,6 @@ function SearchContextProvider({children}: SearchContextProps) {
const [shouldShowSelectAllMatchingItems, setShouldShowSelectAllMatchingItems] = useState(false);
const [searchContextData, setSearchContextData] = useState({...defaultSearchContextData});

const selectedReports = searchContextData.selectedReports;
const selectedTransactions = searchContextData.selectedTransactions;
const selectedTransactionIDs = searchContextData.selectedTransactionIDs;
const currentSearchHash = currentSearchQueryJSON?.hash ?? -1;
const currentRecentSearchHash = currentSearchQueryJSON?.recentSearchHash ?? -1;
const currentSimilarSearchHash = currentSearchQueryJSON?.similarSearchHash ?? -1;
Expand All @@ -119,7 +113,8 @@ function SearchContextProvider({children}: SearchContextProps) {

const {defaultCardFeed} = useCardFeedsForDisplay();
const {accountID} = useCurrentUserPersonalDetails();
const suggestedSearches = getSuggestedSearches(accountID, defaultCardFeed?.id);
const defaultCardFeedID = defaultCardFeed?.id;
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeedID), [accountID, defaultCardFeedID]);

const currentSearchKey = useMemo(() => {
return Object.values(suggestedSearches).find((search) => search.similarSearchHash === currentSimilarSearchHash)?.key;
Expand Down Expand Up @@ -150,7 +145,7 @@ function SearchContextProvider({children}: SearchContextProps) {
return snapshotSearchResults ?? undefined;
}, [currentSearchKey, shouldUseLiveData, snapshotSearchResults, todoSearchResultsData]);

const setSelectedTransactions: SearchActionsContextValue['setSelectedTransactions'] = (transactionIDs, data = []) => {
const setSelectedTransactions: SearchActionsContextValue['setSelectedTransactions'] = useCallback((transactionIDs, data = []) => {
if (transactionIDs instanceof Array) {
if (!transactionIDs.length && areTransactionsEmpty.current) {
areTransactionsEmpty.current = true;
Expand Down Expand Up @@ -211,7 +206,12 @@ function SearchContextProvider({children}: SearchContextProps) {
selectedTransactions: transactionIDs,
shouldTurnOffSelectionMode: false,
}));
};
}, []);

const currentSearchHashRef = useRef(currentSearchHash);
useEffect(() => {
currentSearchHashRef.current = currentSearchHash;
}, [currentSearchHash]);

const setCurrentSelectedTransactionReportID: SearchActionsContextValue['setCurrentSelectedTransactionReportID'] = (reportID) => {
setSearchContextData((prevState) => {
Expand All @@ -233,88 +233,116 @@ function SearchContextProvider({children}: SearchContextProps) {
return;
}

if (searchHashOrClearIDsFlag === currentSearchHash) {
if (searchHashOrClearIDsFlag === currentSearchHashRef.current) {
return;
}

if (selectedReports.length === 0 && isEmptyObject(selectedTransactions) && !searchContextData.shouldTurnOffSelectionMode) {
return;
}
setSearchContextData((prevState) => ({
...prevState,
shouldTurnOffSelectionMode,
selectedTransactions: {},
selectedReports: [],
}));
setSearchContextData((prevState) => {
if (prevState.selectedReports.length === 0 && isEmptyObject(prevState.selectedTransactions) && !prevState.shouldTurnOffSelectionMode) {
return prevState;
}
return {
...prevState,
shouldTurnOffSelectionMode,
selectedTransactions: {},
selectedReports: [],
};
});

// Unselect all transactions and hide the "select all matching items" option
setShouldShowSelectAllMatchingItems(false);
selectAllMatchingItems(false);
},
[currentSearchHash, searchContextData.shouldTurnOffSelectionMode, selectedReports.length, selectedTransactions],
// currentSearchHash is read via currentSearchHashRef to keep this callback stable.
// setShouldShowSelectAllMatchingItems and selectAllMatchingItems are stable useState setters.
[setSelectedTransactions],
);

const removeTransaction: SearchActionsContextValue['removeTransaction'] = (transactionID) => {
const removeTransaction: SearchActionsContextValue['removeTransaction'] = useCallback((transactionID) => {
if (!transactionID) {
return;
}

if (!isEmptyObject(selectedTransactions)) {
const newSelectedTransactions = Object.entries(selectedTransactions).reduce((acc, [key, value]) => {
if (key === transactionID) {
return acc;
}
acc[key] = value;
return acc;
}, {} as SelectedTransactions);
setSearchContextData((prevState) => {
const hasSelectedTransactions = !isEmptyObject(prevState.selectedTransactions);
const hasSelectedIDs = prevState.selectedTransactionIDs.length > 0;

setSearchContextData((prevState) => ({
...prevState,
selectedTransactions: newSelectedTransactions,
}));
}
if (!hasSelectedTransactions && !hasSelectedIDs) {
return prevState;
}

if (selectedTransactionIDs.length > 0) {
setSearchContextData((prevState) => ({
...prevState,
selectedTransactionIDs: selectedTransactionIDs.filter((ID) => transactionID !== ID),
}));
}
};
const newState = {...prevState};
if (hasSelectedTransactions) {
const newSelectedTransactions = Object.entries(prevState.selectedTransactions).reduce((acc, [key, value]) => {
if (key === transactionID) {
return acc;
}
acc[key] = value;
return acc;
}, {} as SelectedTransactions);
newState.selectedTransactions = newSelectedTransactions;
}
if (hasSelectedIDs) {
newState.selectedTransactionIDs = prevState.selectedTransactionIDs.filter((ID) => transactionID !== ID);
}
return newState;
});
}, []);

const setShouldResetSearchQuery = (shouldReset: boolean) => {
const setShouldResetSearchQuery = useCallback((shouldReset: boolean) => {
setSearchContextData((prevState) => ({
...prevState,
shouldResetSearchQuery: shouldReset,
}));
};

const searchStateContextValue: SearchStateContextValue = {
...searchContextData,
suggestedSearches,
currentSearchKey,
currentSearchHash,
currentSimilarSearchHash,
currentSearchResults,
shouldUseLiveData,
shouldShowActionsBarLoading,
lastSearchType,
shouldShowSelectAllMatchingItems,
areAllMatchingItemsSelected,
currentSearchQueryJSON,
};
}, []);

const searchStateContextValue: SearchStateContextValue = useMemo(
() => ({
...searchContextData,
suggestedSearches,
currentSearchKey,
currentSearchHash,
currentSimilarSearchHash,
currentSearchResults,
shouldUseLiveData,
shouldShowActionsBarLoading,
lastSearchType,
shouldShowSelectAllMatchingItems,
areAllMatchingItemsSelected,
currentSearchQueryJSON,
}),
[
searchContextData,
suggestedSearches,
currentSearchKey,
currentSearchHash,
currentSimilarSearchHash,
currentSearchResults,
shouldUseLiveData,
shouldShowActionsBarLoading,
lastSearchType,
shouldShowSelectAllMatchingItems,
areAllMatchingItemsSelected,
currentSearchQueryJSON,
],
);

const searchActionsContextValue: SearchActionsContextValue = {
removeTransaction,
setSelectedTransactions,
setCurrentSelectedTransactionReportID,
clearSelectedTransactions,
setShouldShowActionsBarLoading,
setLastSearchType,
setShouldShowSelectAllMatchingItems,
selectAllMatchingItems,
setShouldResetSearchQuery,
};
const searchActionsContextValue: SearchActionsContextValue = useMemo(
() => ({
removeTransaction,
setSelectedTransactions,
setCurrentSelectedTransactionReportID,
clearSelectedTransactions,
setShouldShowActionsBarLoading,
setLastSearchType,
setShouldShowSelectAllMatchingItems,
selectAllMatchingItems,
setShouldResetSearchQuery,
}),
// setShouldShowActionsBarLoading, setLastSearchType, setShouldShowSelectAllMatchingItems,
// and selectAllMatchingItems are stable useState setters — excluded from deps intentionally.
// setCurrentSelectedTransactionReportID only uses setSearchContextData (stable setter).
[removeTransaction, setSelectedTransactions, clearSelectedTransactions, setShouldResetSearchQuery],
);

return (
<SearchStateContext value={searchStateContextValue}>
Expand Down
Loading
Loading