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
[Due for payment 2026-10-14] Spend - Tag list is empty and does not show previously loaded tags in offline mode #102049
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
👋 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.
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.
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:
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:
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-18title: 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\)'`- ❌
```tsuseEffect(() => {
return () => {
clearSearchTagFiltersState(); // also does Onyx.set(RESULTS, [])
};
}, []);
✅
useEffect(()=>{return()=>{// Reset pagination only; keep cached pages so the list renders offline.setSearchTagFiltersPagination(false,'','');};},[]);
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
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]
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.
Activity
applause-bot commentedon Sep 23, 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/8301ac980cc4208a73c5470872a9b09b8958ed97b2a41d10330bcda79701b9d3
https://platform.applause.com/services/links/v1/external/1e4360481d2132cf3a0286d78bf881cb0c48faf7c377392e077d07073397fb0a
melvin-bot commentedon Sep 23, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #99831
github-actions commentedon Sep 23, 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:
melvin-bot commentedon Sep 23, 2026
@JS00001 @Krishna2323 you have been assigned to this deploy blocker because you reviewed the offending PR: #99831
melvin-bot commentedon Sep 23, 2026
@ShridharGoel you have been assigned to this deploy blocker because you authored the offending PR: #99831
melvin-bot commentedon Sep 23, 2026
💬 A slack conversation has been started in #expensify-open-source
MelvinBot commentedon Sep 23, 2026
🔍 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.tsis far safer than reverting a 30-file PR that also adds a new API command, new Onyx keys,HttpUtilschanges 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.
DeployBlockerCashis correctly applied and stays (App PR caused this).DeployBlockerwas not present, so nothing to remove.📋 Detailed Analysis
Evidence
POLICY_TAGSinTagSelectorwith a server-paginated tag search (OpenSearchTagFiltersPage). Before the PR,TagSelectorbuilt its list straight from thepolicyTags_Onyx collection, which is inherently offline-safe. After the PR the list is driven by a RAM-only Onyx key.2026-09-22T13:49:45Z, which lines up with the9.4.92-0staging 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".shouldShowEmptyTagOptionis gated only on!isSearching && !isInitialLoading— both false offline — so "None" is unshifted onto an otherwise empty array.TagSelector.tsxanduseSearchTagFilters.tsin 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-178src/components/Search/FilterComponents/TagSelector.tsx— L41, L60-61, L76src/libs/actions/Search.ts—clearSearchTagFiltersState, L1142-1145Confirmed via
gh pr view 99831 --json filesthat all three paths are in the PR's file list, and via the commit history for those paths that no later PR modified them.Root Cause
Three pieces of #99831 combine to produce the empty list.
1. The cache is destroyed on unmount.
useSearchTagFiltersregisters an unconditional cleanup:clearSearchTagFiltersState()doesOnyx.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:
That assumption from (1) breaks exactly here: the cache was cleared on the promise of a re-fetch that offline mode then cancels.
searchResultsstays[].3. The offline fallback is hollow for the affected accounts.
TagSelectordoes have a fallback, but it reads a different data source:Server results are written only to
RAM_ONLY_SEARCH_TAG_FILTERS_RESULTSand are never merged back into thePOLICY_TAGScollection.POLICY_TAGSis populated lazily per workspace byopenPolicyTagsPage(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:
useLoadSearchCategoryDatapersists 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.
A remount while online already calls
searchTags(''), which optimisticallySETs the results to[]before repopulating, sohasMore/cursor correctness is preserved and there is no stale-data risk. Worth also considering merging fetched tags intoPOLICY_TAGSso 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 newOFFLINE-*category if the maintainers prefer):9 remaining items
[-]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[/+]melvin-bot commentedon Oct 7, 2026
@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]
Krishna2323 commentedon Oct 8, 2026
@MelvinBot, could you please confirm if this was a pure regression from #99831, or if it was already present on prod in some form?
MelvinBot commentedon Oct 8, 2026
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
TagSelector.tsx:26-36built the list only from thepolicyTags_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 asopenPolicyTagsPage, for example when you open a workspace's Tags page. If that never happened, the list held only "None". This matches whatShridharGoeldescribed.useSearchTagFilters.ts:160-165cleared that key when the filter closed.useSearchTagFilters.ts:172-178skipped the re-fetch offline. The offline fallback then read the samepolicyTags_data prod used. For the reported account, it was empty.view run
Krishna2323 commentedon Oct 8, 2026
The fix reached prod and this was a regression, so closing this as no payment is due here.