Thread here https://expensify.slack.com/archives/C08CZDJFJ77/p1772185682696469
Problem
The Button component currently has 53 props, mixing concerns across three domains:
- Text configuration (5 props):
text, textStyles, textHoverStyles, primaryTextNumberOfLines, secondLineText
- Icon configuration (8 props):
icon, iconRight, iconFill, iconHoverFill, iconStyles, iconRightStyles, iconWrapperStyles, shouldShowRightIcon
- Button behavior/styling (35+ props): sizes, variants, events, layout, keyboard, state
This makes it difficult to extend. For example, in the Domain Control project, we need to place a DotIndicator next to the button text in ButtonWithDropdownMenu for RBR. The dot must appear on the left side of the text, requiring two icons with different fill styles — impossible today without adding yet more props.
Proposal
Apply the Compound Component pattern (composition over configuration). Extract text and icon responsibilities into sub-components:
// New interface
<Button onPress={handlePress} success>
<Button.Icon src={Expensicons.Plus} />
<Button.Text>Create expense</Button.Text>
<Button.Icon src={Expensicons.DownArrow} />
</Button>
// With DotIndicator (the motivating use case)
<Button onPress={handlePress} success>
<Button.Icon src={Expensicons.DotIndicator} fill={theme.danger} />
<Button.Text>Submit</Button.Text>
<Button.Icon src={Expensicons.DownArrow} />
</Button>
Button keeps its own props (styling, events, state, accessibility) but delegates content layout to children.
Benefits
- Extensibility without prop explosion — new content arrangements (multiple icons, indicators, badges) require zero new Button props
- Fewer unnecessary re-renders — individual sub-components can be memoized independently; today, any prop change triggers a full Button re-render
- Clearer API — the JSX tree visually communicates what the button contains
- Easier code review — changes to icon or text behavior are scoped to their sub-component
Migration Strategy
The migration must be incremental to avoid regressions across the ~45 files (72 instances) that use Button today:
- Phase 1 — Build the compound components alongside the existing props API. The old props-based rendering continues to work unchanged. Introduce
Button.Icon, Button.Text (and potentially Button.SecondaryText) as sub-components.
- Phase 2 — Deprecate old text/icon props. Mark the 13 text/icon props as
@deprecated in TypeScript. Add an ESLint rule (or no-restricted-syntax config) that warns on the deprecated props to guide contributors toward the new API.
- Phase 3 — Incremental file-by-file migration. Each PR migrates a handful of files. This can be distributed across contributors.
- Phase 4 — Remove deprecated props once all usages are migrated.
Scope
Issue Owner
Current Issue Owner: @mallenexpensify
Issue Owner
Current Issue Owner: @mallenexpensify
Thread here https://expensify.slack.com/archives/C08CZDJFJ77/p1772185682696469
Problem
The
Buttoncomponent currently has 53 props, mixing concerns across three domains:text,textStyles,textHoverStyles,primaryTextNumberOfLines,secondLineTexticon,iconRight,iconFill,iconHoverFill,iconStyles,iconRightStyles,iconWrapperStyles,shouldShowRightIconThis makes it difficult to extend. For example, in the Domain Control project, we need to place a
DotIndicatornext to the button text inButtonWithDropdownMenufor RBR. The dot must appear on the left side of the text, requiring two icons with different fill styles — impossible today without adding yet more props.Proposal
Apply the Compound Component pattern (composition over configuration). Extract text and icon responsibilities into sub-components:
Button keeps its own props (styling, events, state, accessibility) but delegates content layout to children.
Benefits
Migration Strategy
The migration must be incremental to avoid regressions across the ~45 files (72 instances) that use Button today:
Button.Icon,Button.Text(and potentiallyButton.SecondaryText) as sub-components.@deprecatedin TypeScript. Add an ESLint rule (orno-restricted-syntaxconfig) that warns on the deprecated props to guide contributors toward the new API.Scope
src/components/Button/index.tsx— main componentsrc/components/ButtonWithDropdownMenu/index.tsx— primary consumer that motivates this change<Buttoninstances across the codebaseIssue Owner
Current Issue Owner: @mallenexpensifyIssue Owner
Current Issue Owner: @mallenexpensify