r## Bug Description
Slack thread https://expensify.slack.com/archives/C05LX9D6E07/p1771814813477409
When creating expenses and viewing the Expenses page (Reports > Expenses), newly created expenses flash yellow (highlight animation) correctly on first appearance, but then flash again repeatedly on subsequent search result updates. The highlight should only play once per new expense.
Expected Behavior
Each expense should flash yellow exactly once when it first appears in the Expenses list.
Actual Behavior
The same expense flashes yellow repeatedly whenever search results are refreshed (e.g., due to Onyx updates, API responses, or other transaction mutations while on the page).
Root Cause
There is a race condition between two cleanup useEffects in src/hooks/useSearchHighlightAndScroll.ts, combined with a missing deduplication guard.
Race condition between Effect A and Effect B
Two effects fire when newSearchResultKeys transitions from null to a non-null Set:
- Effect A (lines 234-258):
requestAnimationFrame → setTimeout(300ms) → clears transactionIDsToHighlight in Onyx
- Effect B (lines 268-279):
setTimeout(300ms) → setNewSearchResultKeys(null)
Effect B fires first (~300ms) because Effect A has an extra requestAnimationFrame delay (~16ms). When Effect B fires:
setNewSearchResultKeys(null) triggers a React re-render
- Effect A's cleanup function runs, calling
clearTimeout(timer) and cancelAnimationFrame(animation) — this cancels the pending Onyx clear
- Effect A re-runs with
newSearchResultKeys === null and hits the early return at line 236
- Result:
transactionIDsToHighlight stays {transactionID: true} in Onyx permanently (until the user navigates away from the search type)
Missing deduplication for manual highlights
After the first (correct) highlight, transactionIDsToHighlight is never cleared. Then any subsequent change to searchResults.data re-fires the "detect new items" effect (line 182). The filter at lines 210-213 unconditionally returns true for IDs found in manualHighlightTransactionIDs, bypassing the highlightedIDs deduplication check:
const newTransactionIDs = currentTransactionIDs.filter((id) => {
if (manualHighlightTransactionIDs.has(id)) {
return true; // <-- Always true, bypasses highlightedIDs check
}
// ...
});
This causes newSearchResultKeys to be set again, and the highlight animation replays. The same cleanup race repeats indefinitely.
Proposed Fix
Fix 1 — Eliminate the race condition: Ensure transactionIDsToHighlight is cleared reliably regardless of when newSearchResultKeys is reset. For example, clear transactionIDsToHighlight immediately when the highlight is consumed in the "detect new items" effect (line 230), rather than in a separate effect with a timer. Alternatively, decouple the two cleanups so Effect A doesn't depend on newSearchResultKeys in its dependency array.
Fix 2 — Add deduplication for manual highlights: At line 211, also check highlightedIDs.current.has(id) before returning true:
if (manualHighlightTransactionIDs.has(id)) {
return !highlightedIDs.current.has(id);
}
Reproduction Steps
- Open the Expenses page (Reports > Expenses)
- Create a new expense using the global create button (e.g., Scan, Manual, Distance)
- Observe the expense flashes yellow once (correct)
- Wait for any background data refresh or create another expense
- Observe the previously created expense flashes yellow again (incorrect)
Key Files
src/hooks/useSearchHighlightAndScroll.ts — the highlight tracking and cleanup logic
src/hooks/useAnimatedHighlightStyle/index.ts — the yellow flash animation
src/libs/actions/IOU/index.ts (line 1142) — where transactionIDsToHighlight is set on expense creation
src/libs/actions/Transaction.ts (line 1545) — mergeTransactionIdsHighlightOnSearchRoute Onyx merge function
src/components/Search/index.tsx (lines 1038-1063) — where shouldAnimateInHighlight is derived from newSearchResultKeys
Platform
Web, iOS, Android (all platforms using the Expenses search page)
Issue Owner
Current Issue Owner: @FitseTLT
r## Bug Description
Slack thread https://expensify.slack.com/archives/C05LX9D6E07/p1771814813477409
When creating expenses and viewing the Expenses page (Reports > Expenses), newly created expenses flash yellow (highlight animation) correctly on first appearance, but then flash again repeatedly on subsequent search result updates. The highlight should only play once per new expense.
Expected Behavior
Each expense should flash yellow exactly once when it first appears in the Expenses list.
Actual Behavior
The same expense flashes yellow repeatedly whenever search results are refreshed (e.g., due to Onyx updates, API responses, or other transaction mutations while on the page).
Root Cause
There is a race condition between two cleanup
useEffects insrc/hooks/useSearchHighlightAndScroll.ts, combined with a missing deduplication guard.Race condition between Effect A and Effect B
Two effects fire when
newSearchResultKeystransitions fromnullto a non-null Set:requestAnimationFrame→setTimeout(300ms)→ clearstransactionIDsToHighlightin OnyxsetTimeout(300ms)→setNewSearchResultKeys(null)Effect B fires first (~300ms) because Effect A has an extra
requestAnimationFramedelay (~16ms). When Effect B fires:setNewSearchResultKeys(null)triggers a React re-renderclearTimeout(timer)andcancelAnimationFrame(animation)— this cancels the pending Onyx clearnewSearchResultKeys === nulland hits the early return at line 236transactionIDsToHighlightstays{transactionID: true}in Onyx permanently (until the user navigates away from the search type)Missing deduplication for manual highlights
After the first (correct) highlight,
transactionIDsToHighlightis never cleared. Then any subsequent change tosearchResults.datare-fires the "detect new items" effect (line 182). The filter at lines 210-213 unconditionally returnstruefor IDs found inmanualHighlightTransactionIDs, bypassing thehighlightedIDsdeduplication check:This causes
newSearchResultKeysto be set again, and the highlight animation replays. The same cleanup race repeats indefinitely.Proposed Fix
Fix 1 — Eliminate the race condition: Ensure
transactionIDsToHighlightis cleared reliably regardless of whennewSearchResultKeysis reset. For example, cleartransactionIDsToHighlightimmediately when the highlight is consumed in the "detect new items" effect (line 230), rather than in a separate effect with a timer. Alternatively, decouple the two cleanups so Effect A doesn't depend onnewSearchResultKeysin its dependency array.Fix 2 — Add deduplication for manual highlights: At line 211, also check
highlightedIDs.current.has(id)before returningtrue:Reproduction Steps
Key Files
src/hooks/useSearchHighlightAndScroll.ts— the highlight tracking and cleanup logicsrc/hooks/useAnimatedHighlightStyle/index.ts— the yellow flash animationsrc/libs/actions/IOU/index.ts(line 1142) — wheretransactionIDsToHighlightis set on expense creationsrc/libs/actions/Transaction.ts(line 1545) —mergeTransactionIdsHighlightOnSearchRouteOnyx merge functionsrc/components/Search/index.tsx(lines 1038-1063) — whereshouldAnimateInHighlightis derived fromnewSearchResultKeysPlatform
Web, iOS, Android (all platforms using the Expenses search page)
Issue Owner
Current Issue Owner: @FitseTLT