Skip to content

Button - Composition over Configuration #83762

Description

@mountiny

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

  1. Extensibility without prop explosion — new content arrangements (multiple icons, indicators, badges) require zero new Button props
  2. Fewer unnecessary re-renders — individual sub-components can be memoized independently; today, any prop change triggers a full Button re-render
  3. Clearer API — the JSX tree visually communicates what the button contains
  4. 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:

  1. 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.
  2. 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.
  3. Phase 3 — Incremental file-by-file migration. Each PR migrates a handful of files. This can be distributed across contributors.
  4. Phase 4 — Remove deprecated props once all usages are migrated.

Scope

Issue OwnerCurrent Issue Owner: @mallenexpensify
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

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

Metadata

Metadata

Labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions