Skip to content

[$250] Expenses flash yellow highlight repeatedly instead of once #83209

Description

@mountiny

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): requestAnimationFramesetTimeout(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:

  1. setNewSearchResultKeys(null) triggers a React re-render
  2. Effect A's cleanup function runs, calling clearTimeout(timer) and cancelAnimationFrame(animation)this cancels the pending Onyx clear
  3. Effect A re-runs with newSearchResultKeys === null and hits the early return at line 236
  4. 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

  1. Open the Expenses page (Reports > Expenses)
  2. Create a new expense using the global create button (e.g., Scan, Manual, Distance)
  3. Observe the expense flashes yellow once (correct)
  4. Wait for any background data refresh or create another expense
  5. 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 OwnerCurrent Issue Owner: @FitseTLT

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsOverdueWeeklyKSv2

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions