Repository navigation
[Due for payment 2026-10-18] [$1000] [Tracking] SelectionList ListItem - Composition over Configuration #97971
Description
Composition over configuration: SelectionList ListItem
The
ListItemlayer underSelectionListcarries 33 unique boolean props (47 declarations; 14 are re-declared across the stacked prop types) and is rendered on 155 call sites, spread across two list components (SelectionListandSelectionListWithSections).Since we're actively decomposing large components in the codebase, it's a solid next candidate — the 15 live variants (
UserListItem,InviteMemberListItem,CardListItem, …) copy-paste the same content recipes over and over (title 7×, alternate text 6×, avatar-with-focus/hover-border 2×,rightElement+ focus context 2×, right caret 3×), and the Search result rows (TaskListItem,ChatListItem) each hand-roll their own animated-highlight style bundle.Proposed direction
The interface should be granular (flexibility for the custom rows — Search results, split editors), with the existing variants acting as presets covering most of the app. The 155 call sites need zero code changes — variants just become thin wiring over shared primitives, so every styling/a11y fix lands once and reaches all of them.
State sharing works like the decomposed
Button(see #83762): hover/focus/selection reaches primitives through row context, which is also what lets the render-prop children go away. Sketch:// Preset — the ~90% case: pages keep passing a variant, completely unchanged <SelectionList ListItem={UserListItem} ... /> // Granular — full control for custom rows (e.g. Search results) <ListItem.Pressable item={item} onSelectRow={onSelectRow}> <ListItem.Row style={rowStyle}> <ListItem.Avatar reportID={item.reportID} /> <ListItem.TextColumn> <ListItem.Title text={item.text} /> <ListItem.Subtitle text={item.alternateText} /> </ListItem.TextColumn> <ListItem.RBRIndicator brickRoadIndicator={item.brickRoadIndicator} /> <ListItem.RightCaret /> </ListItem.Row> </ListItem.Pressable> // Style bundle — replaces the ~18 lines of copied highlight wiring per Search row with one hook call const {pressableStyle, pressableWrapperStyle} = useListItemHighlight({shouldHighlight, isSelected, variant: 'searchTable', isLargeScreenWidth, isLastItem});The existing
<BaseListItem>/<SelectableListItem>APIs stay as-is during the migration (eventually they become thin adapters over the composed core), so nothing has to move at once. The prop surface shrinks along the way (what survives is item data, not config), and follow-ups can cut 1–3 boilerplate props from most call sites.Related: #83762 (Button — Composition over Configuration)
Reported in Slack.
Issue Owner
Current Issue Owner: @mallenexpensifyMetadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-][Tracking] SelectionList ListItem - Composition over Configuration[/-][+][$1000] [Tracking] SelectionList ListItem - Composition over Configuration[/+]melvin-bot commentedon Aug 6, 2026
mountiny commentedon Aug 6, 2026
$1000 for the entire decomposition https://expensify.slack.com/archives/C02NK2DQWUX/p1786020653440889
jmusial commentedon Aug 11, 2026
@mountiny PR split for sub issues. After PR 6 I'll reassess and there should be couple PRs of cleaning up redundant props
PR 1 — Core +
SearchQueryListItemSearchQueryListItem.ListItem.Pressable,Row,Title,Subtitle,RBRIndicator,ListItemFocusContext+useListItemFocus.Pressable=OfflineWithFeedback+PressableWithFeedback, press/hover/focus/a11y, zero layout; a11y extracted toutils/getListItemAccessibilityProps.ts.ListItemFocusContextto{isFocused?, isFocusVisible?, isHovered?}and switchMemberRightIcon.useListItemHighlightand convertTaskListItem,ChatListItem; deleteTableListItem(dead code, referenced only bytypes.ts).PR 2 —
SelectableListItemadapter +BaseSelectListItem+TravelDomainListItem+CardListItemBaseSelectListItem/SingleSelectListItem,TravelDomainListItem,CardListItem.SingleSelectWithAvatarListItem.SelectableListItemadapter (checkbox/radio viaListSelectionButton, role derived fromcanSelectMultiple, composed onListItem.Pressable);TextColumn.components/ListCheckbox.tsx,components/ListRadioButton.tsx— pure role-fixing wrappers; callers passrole={CONST.ROLE.CHECKBOX}/{CONST.ROLE.RADIO}onListSelectionButtondirectly.styles.preWrap+ inline{paddingLeft}; muted:styles.colorMuted; subtitlemaxWidthviastyle.PR 3 —
CompactAvatar+ multi-select rowsMultiSelectListItem,UserSelectionListItem.CompactAvatar.item.iconsentry;AVATAR_SIZE.X_SMALL, mention-suggestion container styles.PR 4 — Avatar resolvers +
UserListItemfamilyUserListItemContent,UserListItem,BareUserListItem,InviteMemberListItem.ReportAvatar,UserAvatar,WorkspaceAvatar,RightCaret.item.rightElementrenders as plain child; noRightElementprimitive.UserListItemContentdropsisFocused,isFocusVisible,hovered,shouldDisableHoverStyleprops; hosts pass it as plain child instead of render-prop.isFocusVisible; caret hover fill viagetButtonState, gated byshouldDisableHoverStylethrough context;MemberRightIconbadge on visual focus.PR 5 — Sweep and flag deletion
SpendCategorySelectorListItem,SpendRuleListItem,SplitListItem(the three remaining directBaseListItemwrappers). Nothing else is left: every otherValidListItementry lands in PRs 1-5 or is covered transitively.SplitListItemmigrates its pressable/layout like any variant; only its highlight stays on rawuseAnimatedHighlightStyle.shouldDisplayRBR, prop-levelshouldShowRightCaret(item-level flag fromuseConfirmationSections.tsstays),rightHandSideComponent,FooterComponent,wrapperStyle/testIDforwarding.ListItem/types.tsreduction; drop render-prop compat.PR 6 — Search-row highlight conversions
TransactionListItemWide,TransactionListItemNarrow,TransactionGroupListItem,ExpenseReportListItem.useListItemHighlightparams. (It depends on PR 1 alone and can land any time after it.)SelectionListmountiny commentedon Aug 11, 2026
I will let @roryabraham handle this 🙌
roryabraham commentedon Aug 11, 2026
@MelvinBot create sub-issues for each of the PR 6 phases @jmusial listed in his previous comment. Assign each to @jmusial, @truph01, and myself @roryabraham. Label the issues for PRs 1-2 Daily, 3-6 Weekly
SearchQueryListItem#98358SelectableListItemadapter +BaseSelectListItem+TravelDomainListItem+CardListItem#9835925 remaining items
roryabraham commentedon Oct 6, 2026
@MelvinBot post a status update / progress report on this issue, and on each of the sub-issues
MelvinBot commentedon Oct 6, 2026
Status update — 2026-10-06
All 7 PRs are merged. Phases 1–6 are in production and past their 7-day regression windows. The last one, the dead-props cleanup, went to staging yesterday in 9.5.3-0 and should reach production with the next release. When that ships, the project can move to payment.
No regressions attributed to this project so far. Four deploy blockers were auto-linked to sub-issues, but the investigations traced each one to an unrelated PR:
Next steps
Overdue.Awaiting Paymenthere with the$1,000total, asmallenexpensifyasked.Awaiting Paymentlabels. PR 3 and the cleanup issue don't haveSkip Payment.view run
[-][$1000] [Tracking] SelectionList ListItem - Composition over Configuration[/-][+][Due for payment 2026-10-18] [$1000] [Tracking] SelectionList ListItem - Composition over Configuration[/+]melvin-bot commentedon Oct 18, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)truph01 commentedon Oct 18, 2026
@mallenexpensify It is ready for payment and the total amount is $1000
melvin-bot commentedon Oct 18, 2026
Payment Summary
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Oct 19, 2026
Payment Summary
Contributor+: @truph01 due $1000 via NewDot
Guessing that's correct and that we don't need anything else, inc. test steps. cc @roryabraham