Skip to content

refactor: replace React.Children usage to more composition-friendly alternatives (#4989)#5018

Draft
matkoson wants to merge 6 commits into
callstack:mainfrom
matkoson:refactor/react-children
Draft

refactor: replace React.Children usage to more composition-friendly alternatives (#4989)#5018
matkoson wants to merge 6 commits into
callstack:mainfrom
matkoson:refactor/react-children

Conversation

@matkoson

@matkoson matkoson commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Motivation

Several components inspect, classify, or mutate their children with React.Children + cloneElement. This makes composition hard: children must be direct, prop-mergeable elements of an expected type, so wrapping them (in a memo, a custom component, a helper) silently breaks the injected styling/spacing/behavior, and the injected props are not type-safe. The FAB Menu rewrite (#4963) addressed the same class of problem; this PR migrates the remaining offenders to typed props, context, and container-owned layout.

Approach

No child introspection. Each section component owns its own spacing/styling, cross-cutting state flows through context, and parents own layout (gap, padding, clipping) instead of cloning children. v6 alpha allows breaking changes without deprecation aliases, but user-visible output is preserved for documented usage; where a default that required child introspection is dropped, it is called out as a breaking change.

Changes

List.Accordion

  • Removed the React.Children.map + cloneElement that injected paddingLeft and theme into expanded children.
  • Added ListAccordionContext ({ leftIndent }). List.Item consumes it and indents its own container when it renders no left/right. Ripple/background stay full-width.
  • Wrapped children now keep the indentation behavior. Output preserved for List.Item children (zero snapshot churn).

Dialog

  • Removed the first-child cloneElement that injected marginTop: 24.
  • The Dialog surface owns the top inset (paddingTop: 24); Dialog.Title and Dialog.Icon no longer add their own top offset. Dialog.Icon keeps a 16px bottom gap so icon-to-title spacing is preserved.

Dialog.Actions

  • BREAKING CHANGE: no longer injects compact and uppercase into action buttons. Set them explicitly on each button. Inter-action spacing is now container gap.

Card

  • Removed child counting, sibling displayName scanning, and the per-child cloneElement injection of index / total / siblings / borderRadiusStyles.
  • The card clips inner content to its shape (overflow: hidden + combined border radius) instead of injecting corner styles into Card.Cover / Card.Title.
  • BREAKING CHANGE: Card.Content now uses uniform vertical padding regardless of neighboring sections.

Card.Actions

  • BREAKING CHANGE: no longer auto-assigns mode (previously outlined for the first action, contained for the rest) or compact. Set button props explicitly. Spacing is container gap.

ToggleButton.Row

  • Removed positional Children.count / Children.map / cloneElement first/middle/last border injection.
  • Segmented styling flows through ToggleButtonRowContext; the row is an MD3 segmented container (outline-colored background with hairline dividers and padding, clipped corners), and member ToggleButtons flatten their corners via context. Wrapped ToggleButtons compose correctly.
  • Added an optional theme prop to ToggleButton.Row.

Out of scope (kept — idiomatic, not composition-hostile)

  • Tooltip single-child trigger clone, Chip avatar-prop clone, TouchableRipple / TouchableRipple.native Children.only validation.

Deferred (follow-up PR)

  • Appbar + Appbar/utils still use child counting, displayName filtering, action splitting, and cloneElement injection. Targeted for a separate change (context + explicit author ordering).

Breaking changes (summary)

BREAKING CHANGE: Card.Actions no longer injects mode/compact — pass mode="outlined"/mode="contained" (and compact if needed) explicitly.
BREAKING CHANGE: Dialog.Actions no longer injects compact/uppercase — set them explicitly.
BREAKING CHANGE: Card.Content uses uniform vertical padding regardless of neighbors.
BREAKING CHANGE: ToggleButton.Row segmented styling is reworked to an MD3 container (visual refresh).

Example migration (Card.Actions):

// before
<Card.Actions>
  <Button>Cancel</Button>
  <Button>Ok</Button>
</Card.Actions>

// after
<Card.Actions>
  <Button mode="outlined">Cancel</Button>
  <Button mode="contained">Ok</Button>
</Card.Actions>

Test plan

  • yarn typecheck — pass
  • yarn lint — pass (one pre-existing, unrelated TextInput warning)
  • Scoped unit tests pass, with new characterization tests:
    • yarn test ListAccordion (12) — indent applied/not-applied via context
    • yarn test Dialog (12) — surface top spacing for title/content/icon-first, icon-to-title spacing, no action-prop injection
    • yarn test Card scoped (20) — clip to card shape, uniform Card.Content padding, no Card.Actions prop injection
    • yarn test ToggleButton scoped (7) — segmented styling via context
  • The full yarn test suite has pre-existing animation/native-view baseline failures unrelated to this change (identical set on main); scoped runs are green.
  • Manual visual inspection, before (main) vs after, on iOS (iPhone 16 Pro) and Android (Pixel 9 Pro XL): List.Accordion pixel-identical; Dialog icon-to-title spacing preserved; ToggleButton.Row segmented appearance preserved; Card reflects the intended uniform-padding change. No regressions.

Related: #4989
Reference: #4963

matkoson added 6 commits June 29, 2026 22:41
…th context

List.Accordion used React.Children.map + cloneElement to inject paddingLeft and theme into expanded children, which makes composition harder (children had to be direct, prop-mergeable elements). Replace it with ListAccordionContext: the accordion exposes whether descendants should indent (leftIndent), and List.Item consumes it, applying the indent to its own container so the ripple/background stays full-width. Behavior preserved for List.Item children; theme now flows via context. Adds characterization tests. Refs callstack#4989.
@matkoson matkoson changed the title refactor: replace React.Children composition-hostile APIs (#4989) refactor: replace React.Children usage to more composition-friendly alternatives (#4989) Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant