Skip to content

[Due for payment 2026-10-14] Spend - Tag list is empty and does not show previously loaded tags in offline mode #102049

Closed

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.92-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: #99831
Email or phone of affected tester (no customers): applausetester+khcategory19@applause.expensifail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Mac 26.5 / Chrome
App Component: Search

Action Performed:

Precondition:

  1. Go to staging.new.expensify.com
  2. Go to Spend > Expenses.
  3. Open Filters > Tags.
  4. Go offline.
  5. Open Filters > Tags.

Expected Result:

Tag list will show previously loaded tags.

Actual Result:

Tag list is empty except None option.

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7266968_1790150727124.3.mp4

Tags-2026-09-23_08_06_04.070.csv

View all open jobs on GitHub

Applause Internal Information
Exported by: Julia Ponikarchuk
Bug ID: 7266968

Issue OwnerCurrent Issue Owner: @Krishna2323

Activity

  1. added
    DeployBlockerCashThis issue or pull request should block deployment
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 23, 2026
  2. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    You have been assigned to this deploy blocker because you recently merged this PR: #99831

  3. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    👋 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:

    1. Identify the pull request that introduced this issue and revert it.
    2. Find someone who can quickly fix the issue.
    3. Fix the issue yourself.
  4. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    @JS00001 @Krishna2323 you have been assigned to this deploy blocker because you reviewed the offending PR: #99831

  5. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    @ShridharGoel you have been assigned to this deploy blocker because you authored the offending PR: #99831

  6. melvin-bot commented on Sep 23, 2026

    @melvin-bot

    💬 A slack conversation has been started in #expensify-open-source

  7. MelvinBot commented on Sep 23, 2026

    @MelvinBot
    Contributor

    🔍 Investigation Summary

    Classification: Frontend bug
    Causing PR: [Payment due @Krishna2323] Add paginated tag filters with server-side search - "Add paginated tag filters with server-side search" by @ShridharGoel (High confidence)
    Related Issues: None found

    Recommendation: ROLL FORWARD

    The root cause is a 4-line unmount cleanup that wipes the only cached copy of the tag results, while the fetch effect deliberately no-ops when offline — so the reopened list has nothing to render. A targeted fix in useSearchTagFilters.ts is far safer than reverting a 30-file PR that also adds a new API command, new Onyx keys, HttpUtils changes and app setup wiring.

    Assigned: @ShridharGoel (PR author), @JS00001 and @Krishna2323 (approving reviewers) from highest-confidence causing PR only — already assigned by melvin-bot, no change made
    Labels: No changes. DeployBlockerCash is correctly applied and stays (App PR caused this). DeployBlocker was not present, so nothing to remove.

    📋 Detailed Analysis

    Evidence

    • #99831 replaced the direct Onyx read of POLICY_TAGS in TagSelector with a server-paginated tag search (OpenSearchTagFiltersPage). Before the PR, TagSelector built its list straight from the policyTags_ Onyx collection, which is inherently offline-safe. After the PR the list is driven by a RAM-only Onyx key.
    • Merged 2026-09-22T13:49:45Z, which lines up with the 9.4.92-0 staging build the bug was reported on. It is on the Deploy Checklist: New Expensify 2026-09-23 and not in production, matching "Reproducible in production?: N/A - new feature".
    • The reported symptom is exact, not approximate: offline the list renders only the "None" option. That is precisely what the new code produces, because shouldShowEmptyTagOption is gated only on !isSearching && !isInitialLoading — both false offline — so "None" is unshifted onto an otherwise empty array.
    • Every commit touching TagSelector.tsx and useSearchTagFilters.ts in this release belongs to [Payment due @Krishna2323] Add paginated tag filters with server-side search #99831; no other PR in the checklist touches the Search tag filter path. So the suspect PR is confirmed, not merely assumed.

    Verification

    Affected files (all introduced/modified by #99831):

    • src/hooks/useSearchTagFilters.ts (new file in this PR) — L160-165 and L172-178
    • src/components/Search/FilterComponents/TagSelector.tsx — L41, L60-61, L76
    • src/libs/actions/Search.ts — clearSearchTagFiltersState, L1142-1145

    Confirmed via gh pr view 99831 --json files that all three paths are in the PR's file list, and via the commit history for those paths that no later PR modified them.

    ⚠️ Browser verification was not available for this investigation. Conclusions are from static code analysis and git history only.

    Root Cause

    Three pieces of #99831 combine to produce the empty list.

    1. The cache is destroyed on unmount. useSearchTagFilters registers an unconditional cleanup:

    // Clear persisted pagination and cached pages when the filter closes so a fresh open re-fetches with valid hasMore.
    useEffect(() => {
        return () => {
            clearSearchTagFiltersState();
        };
    }, []);

    clearSearchTagFiltersState() does Onyx.set(ONYXKEYS.RAM_ONLY_SEARCH_TAG_FILTERS_RESULTS, []). So closing the Tags filter in step 3 throws away every tag that was just loaded. The comment's stated intent — "so a fresh open re-fetches with valid hasMore" — silently assumes a re-fetch will always be possible.

    2. Offline, the re-fetch never happens. The fetch effect bails out before requesting anything:

    if (isOffline) {
        prevWasOfflineRef.current = true;
        return;
    }

    That assumption from (1) breaks exactly here: the cache was cleared on the promise of a re-fetch that offline mode then cancels. searchResults stays [].

    3. The offline fallback is hollow for the affected accounts. TagSelector does have a fallback, but it reads a different data source:

    const shouldUseOfflineFallback = isOffline && !hasSearchResults;
    ...
    const policyTagsLists = getAllPolicyValues(policyID?.value?.length ? policyID : undefined, ONYXKEYS.COLLECTION.POLICY_TAGS, allPolicyTags);

    Server results are written only to RAM_ONLY_SEARCH_TAG_FILTERS_RESULTS and are never merged back into the POLICY_TAGS collection. POLICY_TAGS is populated lazily per workspace by openPolicyTagsPage(policyID), so for the repro account — "many workspaces and a lot of tags", i.e. precisely the account this feature exists to serve — most workspaces have no tags in Onyx at all. The fallback therefore yields zero items, and the user sees only "None".

    Note the contrast with the category equivalent: useLoadSearchCategoryData persists its loaded flag in a durable Onyx key (IS_SEARCH_FILTERS_CATEGORY_DATA_LOADED) and does not wipe its data on unmount, which is why categories do not regress the same way.

    Suggested fix

    Smallest change that satisfies the issue's Expected Result: stop discarding the results cache when the component unmounts, and reset only the pagination metadata.

    useEffect(() => {
        return () => {
            // Reset pagination so a fresh open re-fetches with a valid hasMore,
            // but keep cached pages so the list still renders offline.
            setSearchTagFiltersPagination(false, '', '');
        };
    }, []);

    A remount while online already calls searchTags(''), which optimistically SETs the results to [] before repopulating, so hasMore/cursor correctness is preserved and there is no stale-data risk. Worth also considering merging fetched tags into POLICY_TAGS so the offline fallback is actually backed by data.


    🧰 Candidate code review rule (suggestion only — not applied)

    This bug has a reusable, greppable signature, so it may be worth a rule in App/.claude/skills/app-coding-standards. Surfacing as a suggestion only; I have not modified any coding-standards file. There is no existing rule covering offline cache invalidation in the current index.

    Proposed as a new CONSISTENCY-18 (or a new OFFLINE-* category if the maintainers prefer):

    ---
    ruleId: CONSISTENCY-18
    title: Do Not Clear An Offline-Visible Cache On Unmount
    ---
    
    #### [CONSISTENCY-18] Never destroy the only cached copy of server-fetched data in an unmount cleanup when the re-fetch is gated on being online
    - **Condition**: A hook stores server-fetched list data in a RAM-only Onyx key and clears it in a `useEffect` cleanup, while its fetch effect early-returns when `isOffline`. Reset pagination metadata only, or keep a durable fallback that actually contains the same data.
    - **Why**: Clearing on unmount is only safe if a re-fetch is guaranteed. Offline, the re-fetch is skipped, so reopening the screen renders an empty list. Introduced in Expensify/App#99831 and shipped as a deploy blocker (Expensify/App#102049): the Search tag filter showed only the "None" option offline.
    - **Search patterns**: files matching `RAM_ONLY_` that contain both `return () => {` with a `clear*State(` call and `if (isOffline) {` followed by a bare `return;`. E.g. `rg -l 'RAM_ONLY_' src/hooks | xargs rg -l 'clear\w+State\(' | xargs rg -l 'if \(isOffline\)'`
    - ❌
      ```ts
      useEffect(() => {
          return () => {
              clearSearchTagFiltersState(); // also does Onyx.set(RESULTS, [])
          };
      }, []);
    • ✅
      useEffect(() => {
          return () => {
              // Reset pagination only; keep cached pages so the list renders offline.
              setSearchTagFiltersPagination(false, '', '');
          };
      }, []);
    
    </details>
    
  8. 9 remaining items

  9. added and removed
    ReviewingHas a PR in review
    on Oct 7, 2026
  10. changed the title [-]Spend - Tag list is empty and does not show previously loaded tags in offline mode[/-] [+][Due for payment 2026-10-14] Spend - Tag list is empty and does not show previously loaded tags in offline mode[/+] on Oct 7, 2026
  11. melvin-bot commented on Oct 7, 2026

    @melvin-bot

    @Krishna2323

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.4-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-14. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  12. Krishna2323 commented on Oct 8, 2026

    @Krishna2323
    Contributor

    @MelvinBot, could you please confirm if this was a pure regression from #99831, or if it was already present on prod in some form?

  13. MelvinBot commented on Oct 8, 2026

    @MelvinBot
    Contributor

    The specific bug is a regression from [Payment due @Krishna2323] Add paginated tag filters with server-side search. Tags loaded online and then lost offline could not happen on prod before that PR.

    A related limitation already existed on prod. Some workspaces have no tags stored on the device yet. For those workspaces, prod showed only "None", both online and offline.

    Evidence
    • Before the PR, TagSelector.tsx:26-36 built the list only from the policyTags_ Onyx collection. It did not fetch anything and did not check network status, so the list was the same online and offline. The app adds tags to that collection only through actions such as openPolicyTagsPage, for example when you open a workspace's Tags page. If that never happened, the list held only "None". This matches what ShridharGoel described.
    • After the PR, online results came from a server search stored in a RAM-only key. useSearchTagFilters.ts:160-165 cleared that key when the filter closed. useSearchTagFilters.ts:172-178 skipped the re-fetch offline. The offline fallback then read the same policyTags_ data prod used. For the reported account, it was empty.
    • The tester's flow shows the new behavior: tags appeared online in step 3 and disappeared offline in step 5. On prod, step 3 would already have shown only "None" for those workspaces.
    • Preserve tag filters cache offline fixed this by keeping the cached results when the filter closes.

    view run

  14. added
    Skip PaymentSkips all the payment process steps for an issue. Use it for fixed-price projects.
    on Oct 8, 2026
  15. Krishna2323 commented on Oct 8, 2026

    @Krishna2323
    Contributor

    The fix reached prod and this was a regression, so closing this as no payment is due here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2EngineeringSkip PaymentSkips all the payment process steps for an issue. Use it for fixed-price projects.

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions