Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2ab5950
use live data on todos
luacmartins Jan 13, 2026
a037d4a
update logic
luacmartins Jan 13, 2026
b1036dd
fix ts
luacmartins Jan 13, 2026
4037735
Merge branch 'cmartins-useLiveData' into cmartins-updateTodosLiveData
luacmartins Jan 13, 2026
adeb4fe
rm comments
luacmartins Jan 13, 2026
247b2a3
add comment
luacmartins Jan 13, 2026
3886fba
simplify hook call
luacmartins Jan 13, 2026
577e433
fix prettier
luacmartins Jan 14, 2026
1bd41a0
update useOnyx wrapper
luacmartins Jan 14, 2026
48f09fd
fix prettier
luacmartins Jan 14, 2026
3ead57b
Merge branch 'cmartins-useLiveData' into cmartins-updateTodosLiveData
luacmartins Jan 14, 2026
15ab577
disable loading/empty views for todos
luacmartins Jan 14, 2026
4296ca3
fix loading issue
luacmartins Jan 14, 2026
03e6325
fix loading/empty states
luacmartins Jan 14, 2026
dacd377
fix prettier
luacmartins Jan 14, 2026
be13d6e
fix tests
luacmartins Jan 14, 2026
fc6b634
Merge branch 'main' into cmartins-updateTodosLiveData
luacmartins Jan 15, 2026
d6e62c6
Merge branch 'main' into cmartins-updateTodosLiveData
luacmartins Jan 15, 2026
e6f1e05
address comments
luacmartins Jan 15, 2026
1bc7a43
add chat reports
luacmartins Jan 15, 2026
0c25788
resolve conflicts
luacmartins Jan 19, 2026
9d0111d
Merge branch 'main' into cmartins-updateTodosLiveData
luacmartins Jan 20, 2026
78b90a9
update types
luacmartins Jan 20, 2026
6e732e8
compute live metadata
luacmartins Jan 20, 2026
afba425
fix prettier
luacmartins Jan 20, 2026
bd99c27
update has data
luacmartins Jan 20, 2026
6104632
use snapshot metadata
luacmartins Jan 20, 2026
f372cbe
revert changes to footer
luacmartins Jan 20, 2026
760adce
Merge branch 'main' into cmartins-updateTodosLiveData
luacmartins Jan 21, 2026
aac08f7
update footer
luacmartins Jan 21, 2026
37261f4
only use live data for base queries
luacmartins Jan 21, 2026
3e6352b
fix tests
luacmartins Jan 21, 2026
d5adab1
update hook to trigger search on update
luacmartins Jan 22, 2026
cf8824d
do not include chat reports
luacmartins Jan 22, 2026
3f79b84
fix ts
luacmartins Jan 22, 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
59 changes: 56 additions & 3 deletions src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';
import useOnyx from '@hooks/useOnyx';
// 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';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useTodos from '@hooks/useTodos';
import {isMoneyRequestReport} from '@libs/ReportUtils';
import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
import {getSuggestedSearches, isTodoSearch, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
import type {SearchKey} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchResults} from '@src/types/onyx';
import type {SearchResultsInfo} from '@src/types/onyx/SearchResults';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {SearchContextData, SearchContextProps, SearchQueryJSON, SelectedTransactions} from './types';

// Default search info when building from live data
// Used for to-do searches where we build SearchResults from live Onyx data instead of API snapshots
const defaultSearchInfo: SearchResultsInfo = {
offset: 0,
type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT,
status: CONST.SEARCH.STATUS.EXPENSE.ALL,
hasMoreResults: false,
hasResults: true,
isLoading: false,
count: 0,
total: 0,
currency: '',
};

const defaultSearchContextData: SearchContextData = {
currentSearchHash: -1,
currentSearchKey: undefined,
Expand All @@ -29,6 +49,7 @@ const defaultSearchContext: SearchContextProps = {
showSelectAllMatchingItems: false,
shouldShowFiltersBarLoading: false,
currentSearchResults: undefined,
shouldUseLiveData: false,
setLastSearchType: () => {},
setCurrentSearchHashAndKey: () => {},
setCurrentSearchQueryJSON: () => {},
Expand All @@ -51,7 +72,37 @@ function SearchContextProvider({children}: ChildrenProps) {
const [searchContextData, setSearchContextData] = useState(defaultSearchContextData);
const areTransactionsEmpty = useRef(true);

const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true});
const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true});
const {todoSearchResultsData} = useTodos();

const currentSearchKey = searchContextData.currentSearchKey;
const currentSearchHash = searchContextData.currentSearchHash;
const {accountID} = useCurrentUserPersonalDetails();
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID), [accountID]);
const shouldUseLiveData = !!currentSearchKey && isTodoSearch(currentSearchHash, suggestedSearches);

// If viewing a to-do search, use live data from useTodos, otherwise return the snapshot data
// We do this so that we can show the counters for the to-do search results without visiting the specific to-do page, e.g. show `Approve [3]` while viewing the `Submit` to-do search.
const currentSearchResults = useMemo((): SearchResults | undefined => {
if (shouldUseLiveData) {
const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData];
const searchInfo: SearchResultsInfo = {
...(snapshotSearchResults?.search ?? defaultSearchInfo),
count: liveData.metadata.count,
total: liveData.metadata.total,
currency: liveData.metadata.currency,
};
const hasResults = Object.keys(liveData.data).length > 0;
// For to-do searches, always return a valid SearchResults object (even with empty data)
// This ensures we show the empty state instead of loading/blocking views
return {
search: {...searchInfo, isLoading: false, hasResults},
data: liveData.data,
};
}

return snapshotSearchResults ?? undefined;
}, [shouldUseLiveData, currentSearchKey, todoSearchResultsData, snapshotSearchResults]);

const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => {
setSearchContextData((prevState) => {
Expand Down Expand Up @@ -207,6 +258,7 @@ function SearchContextProvider({children}: ChildrenProps) {
() => ({
...searchContextData,
currentSearchResults,
shouldUseLiveData,
removeTransaction,
setCurrentSearchHashAndKey,
setCurrentSearchQueryJSON,
Expand All @@ -225,6 +277,7 @@ function SearchContextProvider({children}: ChildrenProps) {
[
searchContextData,
currentSearchResults,
shouldUseLiveData,
removeTransaction,
setCurrentSearchHashAndKey,
setCurrentSearchQueryJSON,
Expand Down
15 changes: 9 additions & 6 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ function Search({
selectAllMatchingItems,
shouldResetSearchQuery,
setShouldResetSearchQuery,
shouldUseLiveData,
} = useSearchContext();
const [offset, setOffset] = useState(0);

Expand Down Expand Up @@ -267,9 +268,8 @@ function Search({

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

const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]);

const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]);
const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0);

const previousReportActions = usePrevious(reportActions);
Expand Down Expand Up @@ -357,15 +357,18 @@ function Search({
shouldCalculateTotals,
reportActions,
previousReportActions,
shouldUseLiveData,
});

// There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
// we also need to check that the searchResults matches the type and status of the current search
const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON);
const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON);

const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline;

// For to-do searches, we never show loading state since the data is always available locally from Onyx
const shouldShowLoadingState =
!shouldUseLiveData &&
!isOffline &&
(!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0) || (hasErrors && !searchRequestResponseStatusCode));
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
Expand Down Expand Up @@ -862,8 +865,8 @@ function Search({
if (!searchResults?.data) {
return [];
}
return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchResults?.search?.type, validGroupBy);
}, [accountID, searchResults?.data, searchResults?.search?.type, visibleColumns, validGroupBy]);
return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, validGroupBy);
}, [accountID, searchResults?.data, searchDataType, visibleColumns, validGroupBy]);

const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
Expand Down Expand Up @@ -1067,7 +1070,7 @@ function Search({
}

const visibleDataLength = filteredData.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length;
if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults?.search?.type)) {
if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchDataType)) {
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB);
return (
<View style={[shouldUseNarrowLayout ? styles.searchListContentContainerStyles : styles.mt3, styles.flex1]}>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ type SearchContextData = {

type SearchContextProps = SearchContextData & {
currentSearchResults: SearchResults | undefined;
/** Whether we're on a main to-do search and should use live Onyx data instead of snapshots */
shouldUseLiveData: boolean;
setCurrentSearchHashAndKey: (hash: number, key: SearchKey | undefined) => void;
setCurrentSearchQueryJSON: (searchQueryJSON: SearchQueryJSON | undefined) => void;
/** If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise */
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useOnyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@ const useOnyx: OriginalUseOnyx = <TKey extends OnyxKey, TReturnValue = OnyxValue
const isOnSearch = useIsOnSearch();

let currentSearchHash: number | undefined;
let shouldUseLiveData = false;
if (isOnSearch && isSnapshotCompatibleKey) {
const {currentSearchHash: searchContextCurrentSearchHash} = use(SearchContext);
const {currentSearchHash: searchContextCurrentSearchHash, shouldUseLiveData: contextShouldUseLiveData} = use(SearchContext);
currentSearchHash = searchContextCurrentSearchHash;
shouldUseLiveData = !!contextShouldUseLiveData;
}

const useOnyxOptions = options as UseOnyxOptions<OnyxKey, OnyxValue<OnyxKey>> | undefined;
const {selector: selectorProp, ...optionsWithoutSelector} = useOnyxOptions ?? {};

// Determine if we should use snapshot data based on search state and key
const shouldUseSnapshot = isOnSearch && !!currentSearchHash && isSnapshotCompatibleKey;
const shouldUseSnapshot = isOnSearch && !!currentSearchHash && isSnapshotCompatibleKey && !shouldUseLiveData;

// Create selector function that handles both regular and snapshot data
const selector = useMemo(() => {
Expand Down
6 changes: 5 additions & 1 deletion src/hooks/useSearchHighlightAndScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type UseSearchHighlightAndScroll = {
searchKey: SearchKey | undefined;
offset: number;
shouldCalculateTotals: boolean;
shouldUseLiveData: boolean;
};

/**
Expand All @@ -38,6 +39,7 @@ function useSearchHighlightAndScroll({
searchKey,
offset,
shouldCalculateTotals,
shouldUseLiveData,
}: UseSearchHighlightAndScroll) {
const isFocused = useIsFocused();
const {isOffline} = useNetwork();
Expand Down Expand Up @@ -147,12 +149,14 @@ function useSearchHighlightAndScroll({
]);

useEffect(() => {
// For live data, isLoading is always false, so we also need to reset when searchResultsData changes
// For snapshot data, we wait for isLoading to become false after the API call completes
if (searchResults?.search?.isLoading) {
return;
}

searchTriggeredRef.current = false;
}, [searchResults?.search?.isLoading]);
}, [searchResults?.search?.isLoading, shouldUseLiveData, searchResultsData]);

// Initialize the set with existing IDs only once
useEffect(() => {
Expand Down
Loading
Loading