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
[$250] IOU - Expense without category appears in the first place in carousel, then moves to last place #90063
Version Number: 9.3.69-0 Reproducible in staging?: Yes Reproducible in production?: No If this was caught during regression testing, add the test name, ID and link from BrowserStack:#86477 Email or phone of affected tester (no customers):sdjosjdoisjnoisnjio@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Mac 26.2 / Chrome App Component: Money Requests
Action Performed:
Go to staging.new.expensify.com
Go to workspace chat.
Create two expenses with category.
Create another expense without category.
Expected Result:
The expense without category will not change position in the expense carousel.
Actual Result:
The expense without category appears in the first place in the expense carousel, then moves to the last place.
👋 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.
Classification: Frontend bug Causing PR: #86477 - "Fix 85553 rbr carousel sorting" by @abelhailefen (High confidence) Related Issues: #85553
Recommendation: REVERT
PR #86477 introduced RBR-aware sorting directly into the carousel's useMemo for carouselTransactions. The compareByRBR comparator is applied unconditionally on every render of the carousel, which means when a transaction without a category (and therefore no RBR flag) is added, the sort re-runs and repositions it to the end of the carousel after initially rendering it in its insertion position. This explains the observed behavior: the new expense first appears at position one (optimistic insertion), then jumps to the last position (after compareByRBR sorts all RBR transactions to the front).
Assigned: @abelhailefen (PR author), @ikevin127 and @grgia (approving reviewers) Labels: DeployBlockerCash kept (frontend App PR is the cause). No label changes made.
📋 Detailed Analysis
Evidence
The issue is reproducible on staging only (not production), matching version 9.3.69-0 which includes PR Fix 85553 rbr carousel sorting #86477 (merged 2026-05-07, appearing as entry Styling the comments #34 in the StagingDeployCash chronological list).
The bug's description ("expense without category appears first, then moves to last") is a direct consequence of how compareByRBR interacts with optimistic transaction insertion: the expense briefly renders at the top via optimistic update, then carouselTransactions re-sorts and pushes it to the end since it has no RBR flag.
Verification
Affected files: src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx and src/libs/TransactionPreviewUtils.ts
The carouselTransactions memo was changed from a simple .slice(0, 11) to a full sort via compareByRBR before slicing, introducing the positional flicker.
Root Cause
In MoneyRequestReportPreviewContent.tsx, the carouselTransactions memo now sorts all transactions via compareByRBR on every render. When a user adds an expense without a category (no RBR indicator), the sort places it after all RBR-flagged transactions. If other categorized/RBR-free transactions exist, the non-RBR transaction renders momentarily in its original order (from the optimistic Onyx update), then snaps to the back of the list once the sort completes on the next render cycle. Additionally, the compareByRBR sort in the carousel ignores the reportActionsMap parameter (it's passed as undefined), meaning it always applies, even when no RBR context is loaded — unlike the transaction list which correctly skips RBR sorting when not in default sort mode.
When a user creates an expense without a category, the client creates an optimisticmissingCategory violation with showInReview: true (ViolationsUtils.ts:419). This causes transactionHasRBR to return true (TransactionPreviewUtils.ts:484), since hasViolation filters for showInReview === true (TransactionUtils/index.ts:2022). The expense sorts to position 1 (first).
When the server responds, it replaces the optimistic violation data. If the server's missingCategory violation has showInReview: false (or omits the field — which defaults to false), then hasViolation(..., showInReview=true) returns false, RBR disappears, and compareByRBR returns 0 for all transactions. The expense falls to its natural date-based position (last, since it's newest).
A secondary contributing factor: the transaction and its violations are tracked by separate useOnyx hooks, so there may be an intermediate render where the new transaction exists but its violations haven't propagated yet — momentarily showing no RBR and causing the position jump.
What changes do you think we should make in order to solve the problem?
The carousel sort should be stable — when compareByRBR returns 0 (equal RBR status), transactions should retain their previous relative order rather than shifting. This can be done by preserving the original array index as a tiebreaker in the sort at MoneyRequestReportPreviewContent.tsx:362-364 and in the transaction list sort at MoneyRequestReportTransactionList.tsx:296-316.
Additionally, if the server is expected to return missingCategory with showInReview: true but isn't, that server-side gap should be addressed to prevent RBR from flickering off after sync.
What alternative solutions did you explore? (Optional)
An alternative would be to ensure the optimistic violation data exactly matches what the server returns (so no RBR state change occurs on sync). However, this is fragile — any future mismatch would reintroduce the same problem. A stable sort is more robust and prevents visual jumps regardless of violation state changes.
Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.
@grgia I wouldn't label this a blocker, it's a UX regression that's not blocking user flow in any breaking way.
@abelhailefen Looks like the PR had 1 regression (this issue), the procedure in this case can go two ways:
if the PR was reverted due to blocking deploy: the original PR author should reopen a rework with the initial PRs changes + the fix for the regression that was found
if the PR is not an acctual deploy blocker (this doesn't look like a blocker, but CME should confirm this): the original PR author should open a follow-up PR with a fix for the regression (this issue)
Keep an eye on the PR / this issue, if it's reverted then a rework is in order, otherwise a follow-up PR fixing the issue 🙌
This is a solid improvement. When compareByRBR returns 0 (same RBR status), transactions currently have undefined relative order. Adding a getCreated date tiebreaker makes the sort deterministic and fixes the secondary date-editing issue. No concerns here.
Diff 2 — Default category to "Uncategorized" in MoneyRequestBuilder⚠️
The root cause analysis is correct. Here's the mechanism:
Expense created with "" category → !categoryKey is true → optimistic missingCategory violation added (ViolationsUtils.ts:419) → RBR = true → sorted first
Category set to "Uncategorized" → !categoryKey is now false → optimistic violation removed → RBR disappears → expense jumps to the back
Setting category to "Uncategorized" from the start prevents this RBR flip. The fix is viable, but has three concerns:
1. Wrong constant — CONST.SEARCH.CATEGORY_DEFAULT_VALUE is search-specific
This constant lives under SEARCH and is only used in CategoryUtils.ts:136 for search filtering. Using it in transaction creation couples expense creation to search display logic. If this value ever changes for search reasons, it would silently break expense creation. Consider defining a dedicated constant (e.g., CONST.TRANSACTION.DEFAULT_CATEGORY) or at minimum adding a code comment explaining the coupling.
2. Only one code path is patched
The fix only covers getMoneyRequestInformation in MoneyRequestBuilder.ts. But expenses can also be created via:
splitBill / splitBillAndOpenReport (Split.ts) — defaults category to ''
If the same rapid-creation scenario is possible via split expenses, the jump would persist there. Recommend auditing those paths too — or better yet, applying the default in buildOptimisticTransaction (where category defaults to ''), which is the single funnel point for all expense creation. That would be a smaller, more robust fix.
3. Optimistic vs backend violation mismatch
After this fix, an expense created with "Uncategorized" won't get an optimistic missingCategory violation (because !categoryKey is false). But the backend WILL add one (because "Uncategorized" is still considered missing). This means RBR will appear when the BE syncs — but since it'll apply to all uncategorized expenses simultaneously, their relative order won't jump. This is acceptable, but worth noting: there's a brief window where no RBR shows even though the category is effectively missing.
Verdict
Both diffs together form a viable solution for the reported issue. The date tiebreaker (Diff 1) is clean and ready as-is. The category default (Diff 2) correctly addresses the root cause but would benefit from:
Using a transaction-specific constant instead of CONST.SEARCH.CATEGORY_DEFAULT_VALUE
Applying the default in buildOptimisticTransaction instead of getMoneyRequestInformation to cover all expense creation paths in one place
Testing the split-expense path to confirm it's not affected
@MelvinBot Draft a PR for this issue using the following solution:
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
index 792122cc11a..3193f0e8d29 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx@@ -57,7 +57,7 @@ import {startSpan} from '@libs/telemetry/activeSpans';
import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import {compareByRBR} from '@libs/TransactionPreviewUtils';
-import {hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';+import {getCreated, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -160,7 +160,7 @@ function MoneyRequestReportPreviewContent({
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const currentUserDetails = useCurrentUserPersonalDetails();
- const {translate, formatPhoneNumber} = useLocalize();+ const {translate, formatPhoneNumber, localeCompare} = useLocalize();
const {convertToDisplayString} = useCurrencyListActions();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
@@ -357,11 +357,16 @@ function MoneyRequestReportPreviewContent({
if (shouldShowAccessPlaceHolder) {
return [];
}
- const sorted = [...transactions].sort((a, b) =>- compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy),- );+ const sorted = [...transactions].sort((a, b) => {+ const rbrComparison = compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy);+ if (rbrComparison !== 0) {+ return rbrComparison;+ }+ // Tiebreak by date (ascending — oldest first) so position is stable across RBR state changes+ return localeCompare(getCreated(a), getCreated(b));+ });
return sorted.slice(0, 11);
- }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy]);+ }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy, localeCompare]);
const prevCarouselTransactionLength = useRef(0);
useEffect(() => {
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index c41e65cd11f..59c9dc9c5af 100644
--- a/src/libs/TransactionUtils/index.ts+++ b/src/libs/TransactionUtils/index.ts@@ -429,7 +429,9 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T
created = '',
merchant = '',
receipt,
- category = '',+ // Prevent RBR flip and transaction jump: initialize category to 'Uncategorized' instead of+ // empty string so optimistic missing category violation isn't added then removed during backend sync+ category = CONST.SEARCH.CATEGORY_DEFAULT_VALUE,
tag = '',
taxCode = '',
taxAmount = 0,
This was tested manually and it fixes the issue as expected, I went with your suggestion of applying the default in buildOptimisticTransaction (where category defaults to ''), which is the single funnel point for all expense creation. The CONST.SEARCH.CATEGORY_DEFAULT_VALUE is correct here, it won't change and the "Optimistic vs backend violation mismatch" is not an issue.
Make sure to add the manual tests from this issue, with verifying the expected result as last testing step.
changed the title [-]IOU - Expense without category appears in the first place in carousel, then moves to last place[/-][+][$250] IOU - Expense without category appears in the first place in carousel, then moves to last place[/+]on May 22, 2026
This issue has not been updated in over 15 days. @grgia, @ikevin127 eroding to Monthly issue.
P.S. Is everyone reading this sure this is really a near-term priority? Be brave: if you disagree, go ahead and close it out. If someone disagrees, they'll reopen it, and if they don't: one less thing to do!
Activity
melvin-bot commentedon May 8, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #86477
melvin-bot commentedon May 8, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon May 8, 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:
OSBotify commentedon May 8, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: #86477 - "Fix 85553 rbr carousel sorting" by @abelhailefen (High confidence)
Related Issues: #85553
Recommendation: REVERT
PR #86477 introduced RBR-aware sorting directly into the carousel's
useMemoforcarouselTransactions. ThecompareByRBRcomparator is applied unconditionally on every render of the carousel, which means when a transaction without a category (and therefore no RBR flag) is added, the sort re-runs and repositions it to the end of the carousel after initially rendering it in its insertion position. This explains the observed behavior: the new expense first appears at position one (optimistic insertion), then jumps to the last position (aftercompareByRBRsorts all RBR transactions to the front).Assigned: @abelhailefen (PR author), @ikevin127 and @grgia (approving reviewers)
Labels:
DeployBlockerCashkept (frontend App PR is the cause). No label changes made.📋 Detailed Analysis
Evidence
9.3.69-0which includes PR Fix 85553 rbr carousel sorting #86477 (merged 2026-05-07, appearing as entry Styling the comments #34 in the StagingDeployCash chronological list).melvin-botcomment on the issue also directly links PR Fix 85553 rbr carousel sorting #86477 as the causing PR.compareByRBRinteracts with optimistic transaction insertion: the expense briefly renders at the top via optimistic update, thencarouselTransactionsre-sorts and pushes it to the end since it has no RBR flag.Verification
src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsxandsrc/libs/TransactionPreviewUtils.tsMoneyRequestReportPreviewContent.tsx,MoneyRequestReportTransactionList.tsx,TransactionPreviewUtils.ts).carouselTransactionsmemo was changed from a simple.slice(0, 11)to a full sort viacompareByRBRbefore slicing, introducing the positional flicker.Root Cause
In
MoneyRequestReportPreviewContent.tsx, thecarouselTransactionsmemo now sorts all transactions viacompareByRBRon every render. When a user adds an expense without a category (no RBR indicator), the sort places it after all RBR-flagged transactions. If other categorized/RBR-free transactions exist, the non-RBR transaction renders momentarily in its original order (from the optimistic Onyx update), then snaps to the back of the list once the sort completes on the next render cycle. Additionally, thecompareByRBRsort in the carousel ignores thereportActionsMapparameter (it's passed asundefined), meaning it always applies, even when no RBR context is loaded — unlike the transaction list which correctly skips RBR sorting when not in default sort mode.MelvinBot commentedon May 8, 2026
Proposal
What is the root cause of that problem?
PR #86477 introduced
compareByRBRsorting to the expense carousel (MoneyRequestReportPreviewContent.tsx:362). This sorts transactions with RBR indicators (violations) to the front.When a user creates an expense without a category, the client creates an optimistic
missingCategoryviolation withshowInReview: true(ViolationsUtils.ts:419). This causestransactionHasRBRto returntrue(TransactionPreviewUtils.ts:484), sincehasViolationfilters forshowInReview === true(TransactionUtils/index.ts:2022). The expense sorts to position 1 (first).When the server responds, it replaces the optimistic violation data. If the server's
missingCategoryviolation hasshowInReview: false(or omits the field — which defaults tofalse), thenhasViolation(..., showInReview=true)returnsfalse, RBR disappears, andcompareByRBRreturns0for all transactions. The expense falls to its natural date-based position (last, since it's newest).A secondary contributing factor: the transaction and its violations are tracked by separate
useOnyxhooks, so there may be an intermediate render where the new transaction exists but its violations haven't propagated yet — momentarily showing no RBR and causing the position jump.What changes do you think we should make in order to solve the problem?
The carousel sort should be stable — when
compareByRBRreturns0(equal RBR status), transactions should retain their previous relative order rather than shifting. This can be done by preserving the original array index as a tiebreaker in the sort atMoneyRequestReportPreviewContent.tsx:362-364and in the transaction list sort atMoneyRequestReportTransactionList.tsx:296-316.Additionally, if the server is expected to return
missingCategorywithshowInReview: truebut isn't, that server-side gap should be addressed to prevent RBR from flickering off after sync.What alternative solutions did you explore? (Optional)
An alternative would be to ensure the optimistic violation data exactly matches what the server returns (so no RBR state change occurs on sync). However, this is fragile — any future mismatch would reintroduce the same problem. A stable sort is more robust and prevents visual jumps regardless of violation state changes.
Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR,@MelvinBot <your feedback>to refine this analysis, or explain why you are rejecting Melvin's proposal.ikevin127 commentedon May 9, 2026
@grgia I wouldn't label this a blocker, it's a UX regression that's not blocking user flow in any breaking way.
@abelhailefen Looks like the PR had 1 regression (this issue), the procedure in this case can go two ways:
Keep an eye on the PR / this issue, if it's reverted then a rework is in order, otherwise a follow-up PR fixing the issue 🙌
22 remaining items
MelvinBot commentedon May 21, 2026
Review of Proposed Solution
Diff 1 — Date tiebreaker in carousel sort ✅
This is a solid improvement. When
compareByRBRreturns0(same RBR status), transactions currently have undefined relative order. Adding agetCreateddate tiebreaker makes the sort deterministic and fixes the secondary date-editing issue. No concerns here.Diff 2 — Default category to⚠️
"Uncategorized"inMoneyRequestBuilderThe root cause analysis is correct. Here's the mechanism:
""category →!categoryKeyistrue→ optimisticmissingCategoryviolation added (ViolationsUtils.ts:419) → RBR = true → sorted first"Uncategorized"→!categoryKeyis nowfalse→ optimistic violation removed → RBR disappears → expense jumps to the backSetting category to
"Uncategorized"from the start prevents this RBR flip. The fix is viable, but has three concerns:1. Wrong constant —
CONST.SEARCH.CATEGORY_DEFAULT_VALUEis search-specificThis constant lives under
SEARCHand is only used inCategoryUtils.ts:136for search filtering. Using it in transaction creation couples expense creation to search display logic. If this value ever changes for search reasons, it would silently break expense creation. Consider defining a dedicated constant (e.g.,CONST.TRANSACTION.DEFAULT_CATEGORY) or at minimum adding a code comment explaining the coupling.2. Only one code path is patched
The fix only covers
getMoneyRequestInformationinMoneyRequestBuilder.ts. But expenses can also be created via:splitBill/splitBillAndOpenReport(Split.ts) — defaults category to''trackExpense(TrackExpense.ts)sendInvoice(SendInvoice.ts)If the same rapid-creation scenario is possible via split expenses, the jump would persist there. Recommend auditing those paths too — or better yet, applying the default in
buildOptimisticTransaction(wherecategorydefaults to''), which is the single funnel point for all expense creation. That would be a smaller, more robust fix.3. Optimistic vs backend violation mismatch
After this fix, an expense created with
"Uncategorized"won't get an optimisticmissingCategoryviolation (because!categoryKeyisfalse). But the backend WILL add one (because"Uncategorized"is still considered missing). This means RBR will appear when the BE syncs — but since it'll apply to all uncategorized expenses simultaneously, their relative order won't jump. This is acceptable, but worth noting: there's a brief window where no RBR shows even though the category is effectively missing.Verdict
Both diffs together form a viable solution for the reported issue. The date tiebreaker (Diff 1) is clean and ready as-is. The category default (Diff 2) correctly addresses the root cause but would benefit from:
CONST.SEARCH.CATEGORY_DEFAULT_VALUEbuildOptimisticTransactioninstead ofgetMoneyRequestInformationto cover all expense creation paths in one placeikevin127 commentedon May 22, 2026
@MelvinBot Draft a PR for this issue using the following solution:
This was tested manually and it fixes the issue as expected, I went with your suggestion of applying the default in buildOptimisticTransaction (where category defaults to
''), which is the single funnel point for all expense creation. TheCONST.SEARCH.CATEGORY_DEFAULT_VALUEis correct here, it won't change and the "Optimistic vs backend violation mismatch" is not an issue.Make sure to add the manual tests from this issue, with verifying the expected result as last testing step.
MelvinBot commentedon May 22, 2026
🤖 Draft PR created: #91467
Please mark it as "Ready for review" when it is ready for review.
[-]IOU - Expense without category appears in the first place in carousel, then moves to last place[/-][+][$250] IOU - Expense without category appears in the first place in carousel, then moves to last place[/+]melvin-bot commentedon May 22, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022057936884666863998
ikevin127 commentedon May 22, 2026
Payment will be handled in initial issue as this is a regression issue of that initial one.
melvin-bot commentedon Jun 15, 2026
This issue has not been updated in over 15 days. @grgia, @ikevin127 eroding to Monthly issue.
P.S. Is everyone reading this sure this is really a near-term priority? Be brave: if you disagree, go ahead and close it out. If someone disagrees, they'll reopen it, and if they don't: one less thing to do!
ikevin127 commentedon Jun 15, 2026
@grgia This issue should be closed as resolved. More details can be found in the initial issue for confirmation.
grgia commentedon Aug 10, 2026
Confirmed, fixed by #91467 and payment settled in #85553. Closing, thanks @ikevin127!