You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$175] Spend-Expense appears in Spend results despite not matching the applied merchant filter #104147
Version Number: V9.5.10-0 Reproducible in staging?: Yes Reproducible in production?: N/A - new feature, doesn't exist in prod If this was caught during regression testing, add the test name, ID and link from BrowserStack:#83733 Email or phone of affected tester (no customers):[email protected] Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Redmi note 10s android 13 App Component: User Settings
Action Performed:
Launch app
Go to spend
Create expense with merchant I
Tap filter - merchant - Enter I and apply filter
Create expense with merchant Gj
Note expense with merchant Gj is shown under step 4 merchant equal to I filter results
Expected Result:
Only expenses matching the applied Merchant filter (I) should appear in the filtered results.
Actual Result:
The expense with merchant Gj appears in Spend results even though the Merchant filter is set to I.
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Root cause: After you create an expense from Spend, the Search page adds the new transaction to the list on screen. It never checks whether the transaction matches the active filters (useOptimisticSearchTracking.ts:145-185). The server's filtered results correctly leave the "Gj" expense out, so tracking never clears and the row stays. This affects any restrictive filter, not just merchant.
Recommended fix (forward-fix):
In searchDataWithOptimisticTransaction, inject the transaction only when it matches the active queryJSON. Move the matching logic in shouldOptimisticallyUpdateSearch (SearchUpdate.ts:105-128) into a shared helper and call it from both places.
Add a test: with merchant:I active, creating a "Gj" expense from Spend adds no row.
Suggested triage: Ask QA to reproduce this on production with any filter. The merchant filter already exists there; only the Contains/Equal to choice is new. If it reproduces, remove DeployBlockerCash and handle this as a regular bug. 91350-Withdrawn-Last created in Spend expense appears after applying Withdrawn filter reported the same symptom in production in May. It was closed as stale.
Investigation details
Injection has no filter check. The only guard is !searchData || !isTransactionSearchType(type) || !optimisticWatchKey || isOptimisticTrackingCleared. The augmented data goes straight into getSections (useSearchSnapshot.ts:214-217).
The Onyx snapshot update is correctly gated.shouldOptimisticallyUpdateSearch returns false when a merchant filter is set. The hook above bypasses that check.
Inject the optimistic transaction only when it matches the active query. Reuse the matching logic from shouldOptimisticallyUpdateSearch (SearchUpdate.ts:105-128) through a shared helper.
changed the title [-]Spend-Expense appears in Spend results despite not matching the applied merchant filter[/-][+][$175] Spend-Expense appears in Spend results despite not matching the applied merchant filter[/+]on Oct 15, 2026
Please re-state the problem that we are trying to solve in this issue.
An expense created from Spend shows up in a filtered Search list even when it doesn't match the filters. For example, a "Gj" expense appears under merchant:I.
What is the root cause of that problem?
Search adds the new expense to the list on screen right away, before the server responds. It does this without checking the active query.
searchDataWithOptimisticTransaction in useOptimisticSearchTracking.ts:145-185 only checks the search type and whether tracking is active. It never compares the transaction with queryJSON.
What changes do you think we should make in order to solve the problem?
Use the existing shouldOptimisticallyUpdateSearch check in useOptimisticSearchTracking. Look up the report through optimisticTransaction.reportID. If the transaction doesn't match the current queryJSON, return searchData unchanged instead of injecting it.
In the same case, call clearOptimisticTracking() so the pending-expense placeholder and cached row are dropped. useStableOptimisticSortedData then has nothing to re-add.
Add a unit test: with merchant:I active, creating a "Gj" expense from Spend adds no row. With no filters, the row still appears right away.
What alternative solutions did you explore? (Optional)
Copy only the restrictive-filter part of shouldOptimisticallyUpdateSearch into a small shared helper, such as doesQueryAllowOptimisticTransaction, and call it from both places. This avoids passing the invoice flag and current account ID into the hook, but it adds a second entry point that could drift from the first.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/104147#issuecomment-6200594706) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
When a user on the Spend/Search page has an active restrictive filter applied (e.g. merchant:I), creating a new expense whose merchant does not match the filter (e.g. merchant "Gj") causes the non-matching expense to appear in the filtered search results list. Because the server returns only matching items, the newly created non-matching row remains stuck in the search results until the search page remounts.
// Augment search data with the optimistic transaction (before it appears in server snapshot).constsearchDataWithOptimisticTransaction=(()=>{constsearchData=searchResults?.data;if(!searchData||!isTransactionSearchType(type)||!optimisticWatchKey||isOptimisticTrackingCleared){returnsearchData;}constoptimisticTransactionKey=optimisticWatchKey.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)
? (optimisticWatchKeyas `${typeofONYXKEYS.COLLECTION.TRANSACTION}${string}`)
: undefined;constoptimisticTransaction=optimisticTransactionKey ? transactions?.[optimisticTransactionKey] : undefined;if(!optimisticTransactionKey||!optimisticTransaction?.transactionID||searchData[optimisticTransactionKey]||optimisticTransaction.reportID===CONST.REPORT.SPLIT_REPORT_ID){returnsearchData;}constnextSearchData={
...searchData,[optimisticTransactionKey]: optimisticTransaction,}asSearchResults['data'];
...
Blind injection without filter validation: searchDataWithOptimisticTransaction blindly injects optimisticTransaction into nextSearchData solely based on whether isTransactionSearchType(type) is true and a watch key exists. It completely ignores queryJSON and its filters (such as merchant, category, tag, etc.).
Infinite retention in useStableOptimisticSortedData.ts:
In useStableOptimisticSortedData.ts:113-139, tracking is only cleared if the optimistic item appears in the server snapshot (searchResults.data[watchKey]). Because the server correctly filters out non-matching transactions, the item is never present in the server snapshot. Meanwhile, useOptimisticSearchTracking keeps re-injecting the item on every render, preventing it from ever dropping out of the list and triggering rollback.
While SearchUpdate.ts:shouldOptimisticallyUpdateSearch already enforces that searches with restrictive flat filters (like merchant) should not be optimistically updated, useOptimisticSearchTracking bypassed this check entirely.
What changes do you think we should make in order to solve the problem?
1. In src/libs/actions/IOU/SearchUpdate.ts:
Export shouldOptimisticallyUpdateSearch (or a helper doesQueryMatchOptimisticTransaction) so optimistic search tracking can validate the query against the optimistic transaction and report.
2. In src/components/Search/hooks/useOptimisticSearchTracking.ts:
Before augmenting searchData, check whether the query allows optimistic transaction updates. If the query has restrictive filters (such as merchant) that the optimistic transaction does not match, skip injecting optimisticTransaction and call clearOptimisticTracking() so pending placeholders and cached items are cleanly dismissed:
diff --git a/src/components/Search/hooks/useOptimisticSearchTracking.ts b/src/components/Search/hooks/useOptimisticSearchTracking.ts
index e98213a..d8123fa 100644
--- a/src/components/Search/hooks/useOptimisticSearchTracking.ts+++ b/src/components/Search/hooks/useOptimisticSearchTracking.ts@@ -4,6 +4,7 @@ import {flushPendingSearchWrite, getSearchWriteWatchKey, hasPendingSearchWrite}
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {isSearchDataLoaded, isTransactionSearchType} from '@libs/SearchUIUtils';
import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
+import {shouldOptimisticallyUpdateSearch} from '@libs/actions/IOU/SearchUpdate';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -21,6 +22,8 @@ type UseOptimisticSearchTrackingParams = {
searchResults: SearchResults | undefined;
/** Parsed query controlling the active search (type, filters, etc.). */
queryJSON: SearchQueryJSON;
+ /** Current user account ID. */+ currentUserAccountID: number;
/** Full transactions collection used to resolve optimistic watch keys. */
transactions: OnyxCollection<Transaction> | undefined;
/** Report actions collection used to augment search data with optimistic IOU actions. */
@@ -42,6 +45,7 @@ function useOptimisticSearchTracking({searchResults, queryJSON, transactions, re
const {type} = queryJSON;
+ const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const hasPendingWriteOnMount = hasPendingSearchWrite();
const initialWatchKey = hasPendingWriteOnMount ? getSearchWriteWatchKey() : undefined;
@@ -158,6 +162,13 @@ function useOptimisticSearchTracking({searchResults, queryJSON, transactions, re
if (!optimisticTransactionKey || !optimisticTransaction?.transactionID || searchData[optimisticTransactionKey] || optimisticTransaction.reportID === CONST.REPORT.SPLIT_REPORT_ID) {
return searchData;
}
+ const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${optimisticTransaction.reportID}`];+ if (!shouldOptimisticallyUpdateSearch(queryJSON, iouReport, false, currentUserAccountID, optimisticTransaction)) {+ clearOptimisticTracking();+ return searchData;+ }+
const nextSearchData = {
...searchData,
[optimisticTransactionKey]: optimisticTransaction,
3. Unit Tests:
Add a test in tests/unit/Search/useOptimisticSearchTrackingTest.ts:
Verify that when a restrictive filter like merchant:I is active in queryJSON, creating an optimistic transaction with merchant "Gj" does not augment searchData and triggers clearOptimisticTracking().
Verify that when no restrictive filters are present (e.g. default Spend tab type:expense), the optimistic transaction is augmented immediately as expected.
What alternative solutions did you explore? (Optional)
Extracting the filter validation logic into a dedicated helper matchesSearchQueryFilters(queryJSON, transaction, report) in SearchQueryUtils.ts. This would decouple optimistic tracking from SearchUpdate.ts and allow direct unit testing of search query filter evaluation against transactions.
@truph01 I was able to reproduce this and identified two critical issues in MelvinBot's proposal:
setState during render: MelvinBot suggests calling clearOptimisticTracking() where searchData is returned unchanged (searchDataWithOptimisticTransaction). Because this is an inline render-phase calculation, executing clearOptimisticTracking() triggers 3 state setters (setOptimisticWatchKey, setShowPendingExpensePlaceholder, setIsOptimisticTrackingCleared) during render, violating React rules and breaking React Compiler compliance.
Onyx subscription anti-pattern: MelvinBot suggests looking up the report via optimisticTransaction.reportID, but useOptimisticSearchTracking does not have access to reports. Subscribing to ONYXKEYS.COLLECTION.REPORT in Search is a known anti-pattern that causes the entire page to re-render on every chat message in the app. In reality, restrictive flat filters (like merchant) are evaluated purely from queryJSON and do not require reports.
The Clean Fix:
Keep searchDataWithOptimisticTransaction as a pure calculation returning unmodified searchData when ineligible.
Safely call clearOptimisticTracking() inside the existing watch-key useEffect where lifecycle cleanup belongs.
Pass currentUserAccountID from useSearchSnapshot (already in scope) without adding any new Onyx subscriptions.
Repro demo
ReproVedio104147.webm
I have tested this fix locally. Would you like me to post the full proposal?
Please re-state the problem that we are trying to solve in this issue.
When a user has an active restrictive filter applied on Search/Spend (e.g. merchant:I), creating a new expense whose merchant does not match the active filter (e.g. merchant "Gj") causes the non-matching expense to appear in the filtered search results. Because the backend search snapshot excludes the non-matching expense, the optimistic row remains stuck in the search results until the view is reloaded.
What is the root cause of that problem?
In src/components/Search/hooks/useOptimisticSearchTracking.ts:
Lines 146–164 unconditionally inject optimisticTransaction into searchDataWithOptimisticTransaction:
searchDataWithOptimisticTransaction only verifies isTransactionSearchType(type) and whether the tracking key exists. It never validates optimisticTransaction against the active query filters in queryJSON.flatFilters (such as merchant, category, tag, currency).
Calling clearOptimisticTracking() directly inside searchDataWithOptimisticTransaction during render (as proposed in earlier drafts) violates React rules and React Compiler invariants by triggering state updates (setIsOptimisticTrackingCleared, setShowPendingExpensePlaceholder, setOptimisticWatchKey) during render.
The clean place to guard insertion is:
Filter searchDataWithOptimisticTransaction so non-matching optimistic transactions are never injected into the data pipeline.
Clear optimistic tracking cleanly via useEffect without causing unmounted/re-render side effects.
What changes do you think we should make in order to solve the problem?
Add a query matching helper canOptimisticTransactionMatchQuery(transaction, queryJSON) in src/libs/SearchQueryUtils.ts (or SearchUIUtils.ts):
What alternative solutions did you explore? (Optional)
Calling clearOptimisticTracking() inside the searchDataWithOptimisticTransaction IIFE: Rejected because setting state during render violates React rules and breaks React Compiler memoization.
Subscribing to reports in useOptimisticSearchTracking: Rejected because useOptimisticSearchTracking deliberately avoids full report subscriptions to maintain 60 FPS performance on Search screens. Evaluating the transaction's own fields (merchant, category, currency) is fast, zero-overhead, and hermetic.
Activity
applause-bot commentedon Oct 15, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/2d4fecad0bbaefe3187131a39309fd754518f0bc2ae8897f1a73977c36c402ad
melvin-bot commentedon Oct 15, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #83733
melvin-bot commentedon Oct 15, 2026
@neil-marcellini @truph01 you have been assigned to this deploy blocker because you reviewed the offending PR: #83733
melvin-bot commentedon Oct 15, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Oct 15, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
neil-marcellini commentedon Oct 15, 2026
@MelvinBot what's the root cause of this and how might we fix it?
neil-marcellini commentedon Oct 15, 2026
I think we may need to revert that PR because this is the second deploy blocker and this bug looks pretty bad.
Oh wait, but this happens on production 🤔. I bet it's just a problem with how we optimistically add expenses or something. Not a blocker.
2026-10-15_12-07-10.mp4
2 remaining items
MelvinBot commentedon Oct 15, 2026
Deploy Blocker Investigation
Verdict: [Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] is not the cause, so you don't need to revert it. This is an older bug in the client-side optimistic Search tracking. It shipped with [Performance] Dismiss-first navigation for all remaining expense submission paths, merged 2026-05-18.
Root cause: After you create an expense from Spend, the Search page adds the new transaction to the list on screen. It never checks whether the transaction matches the active filters (
useOptimisticSearchTracking.ts:145-185). The server's filtered results correctly leave the "Gj" expense out, so tracking never clears and the row stays. This affects any restrictive filter, not just merchant.Recommended fix (forward-fix):
searchDataWithOptimisticTransaction, inject the transaction only when it matches the activequeryJSON. Move the matching logic inshouldOptimisticallyUpdateSearch(SearchUpdate.ts:105-128) into a shared helper and call it from both places.useStableOptimisticSortedData.ts:113-139, callclearOptimisticTracking()when the transaction doesn't match, so the cached row isn't added back.merchant:Iactive, creating a "Gj" expense from Spend adds no row.Suggested triage: Ask QA to reproduce this on production with any filter. The merchant filter already exists there; only the Contains/Equal to choice is new. If it reproduces, remove
DeployBlockerCashand handle this as a regular bug. 91350-Withdrawn-Last created in Spend expense appears after applying Withdrawn filter reported the same symptom in production in May. It was closed as stale.Investigation details
Injection has no filter check. The only guard is
!searchData || !isTransactionSearchType(type) || !optimisticWatchKey || isOptimisticTrackingCleared. The augmented data goes straight intogetSections(useSearchSnapshot.ts:214-217).It runs on every create from Spend.
markPendingWriteForSearchPage.ts:10-15marks a pending write for any global-create flow started outside a report.Tracking never ends in filtered views. Tracking clears in two cases only:
The 10s timeout (
useOptimisticSearchTracking.ts:78-85) only hides the loading placeholder.The Onyx snapshot update is correctly gated.
shouldOptimisticallyUpdateSearchreturns false when a merchant filter is set. The hook above bypasses that check.Why [Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733 is not the cause:
SearchUpdate.ts.merchant:still reaches the backend ascontains, as before.Confidence: High that [Payment due @truph01] fix: Add Contains / Equal to match type for the Merchant filter [80823] #83733 is not involved. Medium-high on the root cause. This comes from reading the code; I haven't reproduced it in the app.
Next Steps: Reply with
@MelvinBot implement thisto create a draft PR, or@MelvinBot <your feedback>to refine this analysis.view run
truph01 commentedon Oct 15, 2026
@MelvinBot Post proposal
MelvinBot commentedon Oct 15, 2026
neil-marcellini, I answered this in my earlier comment. Your production repro matches what I found there.Root cause: After you create an expense from Spend, Search adds it to the list on screen without checking the active filters (
useOptimisticSearchTracking.ts:145-185). The server leaves the expense out of the filtered results, so tracking never clears and the row stays. This came from [Performance] Dismiss-first navigation for all remaining expense submission paths, not the merchant filter PR.Fix:
shouldOptimisticallyUpdateSearch(SearchUpdate.ts:105-128) through a shared helper.clearOptimisticTracking()inuseStableOptimisticSortedData.ts:113-139when the transaction doesn't match.merchant:Iactive, creating a "Gj" expense adds no row.Next Steps: Reply with
@MelvinBot implement thisto create a draft PR, or@MelvinBot <your feedback>to refine this analysis.view run
melvin-bot commentedon Oct 15, 2026
Current assignee @truph01 is eligible for the External assigner, not assigning anyone new.
[-]Spend-Expense appears in Spend results despite not matching the applied merchant filter[/-][+][$175] Spend-Expense appears in Spend results despite not matching the applied merchant filter[/+]melvin-bot commentedon Oct 15, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022110814357178776295
MelvinBot commentedon Oct 15, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
An expense created from Spend shows up in a filtered Search list even when it doesn't match the filters. For example, a "Gj" expense appears under
merchant:I.What is the root cause of that problem?
Search adds the new expense to the list on screen right away, before the server responds. It does this without checking the active query.
searchDataWithOptimisticTransactioninuseOptimisticSearchTracking.ts:145-185only checks the search type and whether tracking is active. It never compares the transaction withqueryJSON.useStableOptimisticSortedData.ts:113-139ends tracking in two cases only:As a result, the row stays until Search remounts.
The Onyx snapshot update already has this check:
shouldOptimisticallyUpdateSearchreturns false for restrictive filters likemerchant. The tracking hook bypasses it. The bug came from [Performance] Dismiss-first navigation for all remaining expense submission paths and already exists on production.What changes do you think we should make in order to solve the problem?
shouldOptimisticallyUpdateSearchcheck inuseOptimisticSearchTracking. Look up the report throughoptimisticTransaction.reportID. If the transaction doesn't match the currentqueryJSON, returnsearchDataunchanged instead of injecting it.clearOptimisticTracking()so the pending-expense placeholder and cached row are dropped.useStableOptimisticSortedDatathen has nothing to re-add.merchant:Iactive, creating a "Gj" expense from Spend adds no row. With no filters, the row still appears right away.What alternative solutions did you explore? (Optional)
Copy only the restrictive-filter part of
shouldOptimisticallyUpdateSearchinto a small shared helper, such asdoesQueryAllowOptimisticTransaction, and call it from both places. This avoids passing the invoice flag and current account ID into the hook, but it adds a second entry point that could drift from the first.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/104147#issuecomment-6200594706)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
janiya2 commentedon Oct 15, 2026
Contributor details
Your Expensify account email: [email protected]
Upwork Profile Link: https://www.upwork.com/freelancers/janithad
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a user on the Spend/Search page has an active restrictive filter applied (e.g.
merchant:I), creating a new expense whose merchant does not match the filter (e.g. merchant "Gj") causes the non-matching expense to appear in the filtered search results list. Because the server returns only matching items, the newly created non-matching row remains stuck in the search results until the search page remounts.What is the root cause of that problem?
In
src/components/Search/hooks/useOptimisticSearchTracking.ts:searchDataWithOptimisticTransactionblindly injectsoptimisticTransactionintonextSearchDatasolely based on whetherisTransactionSearchType(type)is true and a watch key exists. It completely ignoresqueryJSONand its filters (such asmerchant,category,tag, etc.).useStableOptimisticSortedData.ts:In
useStableOptimisticSortedData.ts:113-139, tracking is only cleared if the optimistic item appears in the server snapshot (searchResults.data[watchKey]). Because the server correctly filters out non-matching transactions, the item is never present in the server snapshot. Meanwhile,useOptimisticSearchTrackingkeeps re-injecting the item on every render, preventing it from ever dropping out of the list and triggering rollback.While
SearchUpdate.ts:shouldOptimisticallyUpdateSearchalready enforces that searches with restrictive flat filters (likemerchant) should not be optimistically updated,useOptimisticSearchTrackingbypassed this check entirely.What changes do you think we should make in order to solve the problem?
1. In
src/libs/actions/IOU/SearchUpdate.ts:Export
shouldOptimisticallyUpdateSearch(or a helperdoesQueryMatchOptimisticTransaction) so optimistic search tracking can validate the query against the optimistic transaction and report.2. In
src/components/Search/hooks/useOptimisticSearchTracking.ts:Before augmenting
searchData, check whether the query allows optimistic transaction updates. If the query has restrictive filters (such asmerchant) that the optimistic transaction does not match, skip injectingoptimisticTransactionand callclearOptimisticTracking()so pending placeholders and cached items are cleanly dismissed:3. Unit Tests:
Add a test in
tests/unit/Search/useOptimisticSearchTrackingTest.ts:merchant:Iis active inqueryJSON, creating an optimistic transaction with merchant "Gj" does not augmentsearchDataand triggersclearOptimisticTracking().type:expense), the optimistic transaction is augmented immediately as expected.What alternative solutions did you explore? (Optional)
Extracting the filter validation logic into a dedicated helper
matchesSearchQueryFilters(queryJSON, transaction, report)inSearchQueryUtils.ts. This would decouple optimistic tracking fromSearchUpdate.tsand allow direct unit testing of search query filter evaluation against transactions.melvin-bot commentedon Oct 15, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
emkhalid commentedon Oct 16, 2026
@truph01 I was able to reproduce this and identified two critical issues in MelvinBot's proposal:
setStateduring render: MelvinBot suggests callingclearOptimisticTracking()wheresearchDatais returned unchanged (searchDataWithOptimisticTransaction). Because this is an inline render-phase calculation, executingclearOptimisticTracking()triggers 3 state setters (setOptimisticWatchKey,setShowPendingExpensePlaceholder,setIsOptimisticTrackingCleared) during render, violating React rules and breaking React Compiler compliance.optimisticTransaction.reportID, butuseOptimisticSearchTrackingdoes not have access to reports. Subscribing toONYXKEYS.COLLECTION.REPORTin Search is a known anti-pattern that causes the entire page to re-render on every chat message in the app. In reality, restrictive flat filters (likemerchant) are evaluated purely fromqueryJSONand do not require reports.The Clean Fix:
searchDataWithOptimisticTransactionas a pure calculation returning unmodifiedsearchDatawhen ineligible.clearOptimisticTracking()inside the existing watch-keyuseEffectwhere lifecycle cleanup belongs.currentUserAccountIDfromuseSearchSnapshot(already in scope) without adding any new Onyx subscriptions.Repro demo
ReproVedio104147.webm
I have tested this fix locally. Would you like me to post the full proposal?
Fix demo
FixedVedio104147.webm
jloa-dev commentedon Oct 17, 2026
Contributor details
Your Expensify account email: [email protected]
Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba22190aa
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a user has an active restrictive filter applied on Search/Spend (e.g.
merchant:I), creating a new expense whose merchant does not match the active filter (e.g. merchant "Gj") causes the non-matching expense to appear in the filtered search results. Because the backend search snapshot excludes the non-matching expense, the optimistic row remains stuck in the search results until the view is reloaded.What is the root cause of that problem?
In
src/components/Search/hooks/useOptimisticSearchTracking.ts:Lines 146–164 unconditionally inject
optimisticTransactionintosearchDataWithOptimisticTransaction:searchDataWithOptimisticTransactiononly verifiesisTransactionSearchType(type)and whether the tracking key exists. It never validatesoptimisticTransactionagainst the active query filters inqueryJSON.flatFilters(such asmerchant,category,tag,currency).clearOptimisticTracking()directly insidesearchDataWithOptimisticTransactionduring render (as proposed in earlier drafts) violates React rules and React Compiler invariants by triggering state updates (setIsOptimisticTrackingCleared,setShowPendingExpensePlaceholder,setOptimisticWatchKey) during render.searchDataWithOptimisticTransactionso non-matching optimistic transactions are never injected into the data pipeline.useEffectwithout causing unmounted/re-render side effects.What changes do you think we should make in order to solve the problem?
canOptimisticTransactionMatchQuery(transaction, queryJSON)insrc/libs/SearchQueryUtils.ts(orSearchUIUtils.ts):src/components/Search/hooks/useOptimisticSearchTracking.ts:searchDataWithOptimisticTransaction:useEffect(cleanly disarming tracking when the created expense does not belong in this search without callingsetStateduring render):What alternative solutions did you explore? (Optional)
clearOptimisticTracking()inside thesearchDataWithOptimisticTransactionIIFE: Rejected because setting state during render violates React rules and breaks React Compiler memoization.useOptimisticSearchTracking: Rejected becauseuseOptimisticSearchTrackingdeliberately avoids full report subscriptions to maintain 60 FPS performance on Search screens. Evaluating the transaction's own fields (merchant,category,currency) is fast, zero-overhead, and hermetic.melvin-bot commentedon Oct 17, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
melvin-bot commentedon Oct 20, 2026
@truph01 Huh... This is 4 days overdue. Who can take care of this?