From aaaf84fbae47de065c6970f01b0e853bdb302dcf Mon Sep 17 00:00:00 2001 From: borys3kk Date: Tue, 28 Apr 2026 12:45:37 +0200 Subject: [PATCH 1/2] fix pills problems --- .../SearchAdvancedFiltersButton.tsx | 3 +- .../SearchPageHeader/useSearchFiltersBar.tsx | 8 +++ src/hooks/useSearchFilterSync.ts | 55 ++++++++++--------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx b/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx index 33599b21aa49..19d5d0a2b491 100644 --- a/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx @@ -26,7 +26,8 @@ function SearchAdvancedFiltersButton({queryJSON}: SearchAdvancedFiltersButtonPro const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter']); const filterFormValues = useFilterFormValues(queryJSON); - useSearchFilterSync(filterFormValues); + console.log('[fix-first-filter-pill] SearchAdvancedFiltersButton render', {queryHash: queryJSON?.hash, inputQuery: queryJSON?.inputQuery, filterFormValues}); + useSearchFilterSync(queryJSON, filterFormValues); const openAdvancedFilters = () => { updateAdvancedFilters(filterFormValues); diff --git a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx index 01227e6d2fb9..4d7307df2c6e 100644 --- a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx @@ -191,6 +191,14 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes }); }; + console.log('[fix-first-filter-pill] useSearchFiltersBar render', { + formKeys: Object.keys(searchAdvancedFiltersForm ?? {}), + formDateAfter: searchAdvancedFiltersForm?.dateAfter, + formDateOn: searchAdvancedFiltersForm?.dateOn, + formDateBefore: searchAdvancedFiltersForm?.dateBefore, + formDateRange: searchAdvancedFiltersForm?.dateRange, + inputQuery: queryJSON?.inputQuery, + }); const filters = mapFiltersFormToLabelValueList(searchAdvancedFiltersForm, queryJSON.policyID, SKIPPED_FILTERS, translate, localeCompare, (filterKey) => { const groupConfig = FILTER_GROUP_MAP[filterKey]; if (groupConfig) { diff --git a/src/hooks/useSearchFilterSync.ts b/src/hooks/useSearchFilterSync.ts index fc870981c3a8..25d479391b31 100644 --- a/src/hooks/useSearchFilterSync.ts +++ b/src/hooks/useSearchFilterSync.ts @@ -1,43 +1,48 @@ import {useIsFocused} from '@react-navigation/native'; -import {useEffect, useRef} from 'react'; +import {useEffect} from 'react'; +import type {SearchQueryJSON} from '@components/Search/types'; import {updateAdvancedFilters} from '@libs/actions/Search'; +import {buildSearchQueryString} from '@libs/SearchQueryUtils'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; /** - * Syncs computed filter form values to the SEARCH_ADVANCED_FILTERS_FORM Onyx key - * whenever they change. Call from SearchAdvanceFiltersButton which already computes formValues - * via useFilterFormValues. - * - * The isFocused guard prevents the blurred (previous) SearchPage instance—kept - * mounted during navigation transition animations—from overwriting the Onyx form - * state that was already written by the newly focused instance. - * - * On narrow layout (iOS native), navigating to Advanced Filters causes this screen - * to lose focus. When the user returns, the screen regains focus and this effect - * would re-fire. Without the prevIsFocusedRef guard below, it would overwrite any - * form changes made by Advanced Filters (e.g. resetting a date range) with stale - * values derived from the unchanged URL query parameter. + * Module-level: tracks the last URL query signature that was synced into the + * SEARCH_ADVANCED_FILTERS_FORM Onyx key. We deliberately keep this outside the + * hook so it survives unmount/remount of SearchAdvancedFiltersButton. The + * button is gated by SearchActionsBarSwitch (`showStatic` flips during + * `startTransition` after navigation) which causes the hook to remount and + * would otherwise reset a per-component ref to null — causing the sync to + * fire again with form values derived from the *old* URL, clobbering any + * Onyx.merge that was just done by the Advanced Filters flow (e.g. Save Date + * before the URL has been replaced by View Results). */ -function useSearchFilterSync(formValues: Partial) { +let lastSyncedQuerySig: string | null = null; + +/** + * Syncs computed filter form values to the SEARCH_ADVANCED_FILTERS_FORM Onyx + * key when the URL query string actually changes. The form is the source for + * the filter pills shown in the Search header — overwriting it on every + * re-render (or every fresh mount with the same URL) would erase concurrent + * Onyx.merge writes from Advanced Filters and leave the pill missing. + */ +function useSearchFilterSync(queryJSON: SearchQueryJSON | undefined, formValues: Partial) { const isFocused = useIsFocused(); - const prevIsFocusedRef = useRef(isFocused); useEffect(() => { - const wasPreviouslyFocused = prevIsFocusedRef.current; - prevIsFocusedRef.current = isFocused; - + const querySig = queryJSON ? buildSearchQueryString(queryJSON) : null; + console.log('[fix-first-filter-pill] sync useEffect run', {isFocused, querySig, lastSyncedQuerySig, formValues}); if (!isFocused) { + console.log('[fix-first-filter-pill] sync SKIP not focused'); return; } - - // Skip syncing when just regaining focus to avoid overwriting form - // changes made while this screen was unfocused (e.g. Advanced Filters). - if (!wasPreviouslyFocused) { + if (lastSyncedQuerySig === querySig) { + console.log('[fix-first-filter-pill] sync SKIP same querySig'); return; } - + console.log('[fix-first-filter-pill] sync FIRE Onyx.set', {lastSyncedQuerySig, newQuerySig: querySig, formValues}); + lastSyncedQuerySig = querySig; updateAdvancedFilters(formValues, true); - }, [formValues, isFocused]); + }, [queryJSON, formValues, isFocused]); } export default useSearchFilterSync; From 90f9d66449f2c002dbe36efa6342ef333e75fad0 Mon Sep 17 00:00:00 2001 From: borys3kk Date: Tue, 28 Apr 2026 13:36:18 +0200 Subject: [PATCH 2/2] remove debug logs --- .../SearchPageHeader/SearchAdvancedFiltersButton.tsx | 1 - .../Search/SearchPageHeader/useSearchFiltersBar.tsx | 8 -------- src/hooks/useSearchFilterSync.ts | 6 +----- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx b/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx index 19d5d0a2b491..f701468e8fa4 100644 --- a/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchAdvancedFiltersButton.tsx @@ -26,7 +26,6 @@ function SearchAdvancedFiltersButton({queryJSON}: SearchAdvancedFiltersButtonPro const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter']); const filterFormValues = useFilterFormValues(queryJSON); - console.log('[fix-first-filter-pill] SearchAdvancedFiltersButton render', {queryHash: queryJSON?.hash, inputQuery: queryJSON?.inputQuery, filterFormValues}); useSearchFilterSync(queryJSON, filterFormValues); const openAdvancedFilters = () => { diff --git a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx index 4d7307df2c6e..01227e6d2fb9 100644 --- a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx @@ -191,14 +191,6 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes }); }; - console.log('[fix-first-filter-pill] useSearchFiltersBar render', { - formKeys: Object.keys(searchAdvancedFiltersForm ?? {}), - formDateAfter: searchAdvancedFiltersForm?.dateAfter, - formDateOn: searchAdvancedFiltersForm?.dateOn, - formDateBefore: searchAdvancedFiltersForm?.dateBefore, - formDateRange: searchAdvancedFiltersForm?.dateRange, - inputQuery: queryJSON?.inputQuery, - }); const filters = mapFiltersFormToLabelValueList(searchAdvancedFiltersForm, queryJSON.policyID, SKIPPED_FILTERS, translate, localeCompare, (filterKey) => { const groupConfig = FILTER_GROUP_MAP[filterKey]; if (groupConfig) { diff --git a/src/hooks/useSearchFilterSync.ts b/src/hooks/useSearchFilterSync.ts index 25d479391b31..90e752988509 100644 --- a/src/hooks/useSearchFilterSync.ts +++ b/src/hooks/useSearchFilterSync.ts @@ -29,17 +29,13 @@ function useSearchFilterSync(queryJSON: SearchQueryJSON | undefined, formValues: const isFocused = useIsFocused(); useEffect(() => { - const querySig = queryJSON ? buildSearchQueryString(queryJSON) : null; - console.log('[fix-first-filter-pill] sync useEffect run', {isFocused, querySig, lastSyncedQuerySig, formValues}); if (!isFocused) { - console.log('[fix-first-filter-pill] sync SKIP not focused'); return; } + const querySig = queryJSON ? buildSearchQueryString(queryJSON) : null; if (lastSyncedQuerySig === querySig) { - console.log('[fix-first-filter-pill] sync SKIP same querySig'); return; } - console.log('[fix-first-filter-pill] sync FIRE Onyx.set', {lastSyncedQuerySig, newQuerySig: querySig, formValues}); lastSyncedQuerySig = querySig; updateAdvancedFilters(formValues, true); }, [queryJSON, formValues, isFocused]);