Skip to content

refactor: remove splitStyles and StyleSheet.flatten - #5114

Open
waterim wants to merge 2 commits into
callstack:mainfrom
waterim:refactor/remove-style-flatten
Open

waterim wants to merge 2 commits into
callstack:mainfrom
waterim:refactor/remove-style-flatten

Conversation

@waterim

@waterim waterim commented Sep 10, 2026

Copy link
Copy Markdown

Motivation

A few components still read values out of the style prop with StyleSheet.flatten (and splitStyles on top of it) to derive other styles: background color, border radius, border color, icon placement and so on. This doesn't work with styles returned from useAnimatedStyle, they can't be flattened, so these components couldn't get full Reanimated support.

This PR removes splitStyles and every StyleSheet.flatten call from the library and moves the values we need into props, following what was done for Surface in #5078.

  • Appbar / Appbar.Header: new backgroundColor prop and the same border radius props as Surface. style now accepts animated styles. getAppbarBorders is gone. Appbar now includes safeAreaInsets in its height, so Appbar.Header no longer needs to read height from style to add the status bar inset.
  • Avatar.Icon / Avatar.Image / Avatar.Text: new backgroundColor prop. Contrast color for the text and icon is still derived from it.
  • Button: new iconPosition prop (leading | trailing) instead of detecting flexDirection: 'row-reverse' in contentStyle. Icon color and size no longer come from labelStyle, textColor covers the color.
  • Card: outline color comes from theme.colors.outline instead of style.borderColor.
  • Card.Cover: border radius from style is applied to the container only, the image is clipped by it anyway.
  • BottomNavigation.Bar: background color moved to the outer view, so barStyle={{ backgroundColor }} keeps working without parsing the style.
  • Tooltip: instead of reading position / top / left from the child's style, it attaches a ref to the wrapped element and measures it directly (falling back to the wrapper). The user's ref is still forwarded.
  • DataTable.Pagination and the example app updated to the new props.
  • Added an eslint rule that bans StyleSheet.flatten in src so it doesn't come back.

Breaking changes are listed in the 6.x migration guide.

Related issue

Part of the Reanimated migration for v6. Note that #4943 also adds iconPosition to Button with the same name, whichever lands second will need a small rebase.

Test plan

  • yarn typecheck, yarn lint and yarn jest pass.
  • Added tests for the new Appbar props and for ref forwarding in Tooltip, updated the Avatar, Button and Card tests to use the new props.
  • Snapshot changes are limited to the moved background color in BottomNavigation.Bar, the row-reverse content style in Button and DataTable.Pagination, and {} turning into undefined where the flattened style used to be.
  • Things worth checking in the example app: Appbar screen (custom color, bottom appbar), Avatar screen, Button "Icon right" and the DataTable pagination dropdown.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate issues remain in Avatar background-style precedence and Tooltip ref forwarding and measurement fallback.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Refactors v6 components to remove style flattening and improve Reanimated animated-style support.

Changes:

  • Replaces style-derived values with explicit component props.
  • Updates Tooltip measurement and ref handling.
  • Updates tests, snapshots, examples, linting, and migration documentation.
File summaries
File Summary
src/utils/splitStyles.ts Removes obsolete utility.
src/utils/__tests__/splitStyles.test.ts Removes obsolete tests.
src/components/Tooltip/utils.ts Supports direct measurement.
src/components/Tooltip/Tooltip.tsx Adds ref forwarding and measurement.
src/components/Surface.tsx Exports shared visual prop types.
src/components/DataTable/DataTablePagination.tsx Uses trailing icon positioning.
src/components/Card/utils.tsx Removes style-derived radius handling.
src/components/Card/CardCover.tsx Applies clipping radius to the container.
src/components/Card/Card.tsx Uses themed outline color.
src/components/Button/utils.tsx Uses static border-radius keys.
src/components/Button/Button.tsx Adds explicit icon positioning and styling.
src/components/BottomNavigation/BottomNavigationBar.tsx Moves background styling to the outer bar.
src/components/Avatar/AvatarText.tsx Adds an explicit background color prop.
src/components/Avatar/AvatarImage.tsx Adds an explicit background color prop.
src/components/Avatar/AvatarIcon.tsx Adds an explicit background color prop.
src/components/Appbar/utils.ts Removes border extraction.
src/components/Appbar/AppbarHeader.tsx Updates header styling and safe-area handling.
src/components/Appbar/Appbar.tsx Adds visual props and inset-aware sizing.
src/components/__tests__/Tooltip.test.tsx Tests ref forwarding.
src/components/__tests__/Card/Card.test.tsx Updates Card behavior tests.
src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap Updates Card snapshots.
src/components/__tests__/Button.test.tsx Tests icon positioning.
src/components/__tests__/Avatar.test.tsx Uses background color props.
src/components/__tests__/Appbar/Appbar.test.tsx Tests Appbar visual props.
src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap Updates Appbar snapshots.
src/components/__tests__/__snapshots__/DataTable.test.tsx.snap Updates pagination snapshot.
src/components/__tests__/__snapshots__/Button.test.tsx.snap Updates Button snapshot.
src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap Updates navigation snapshots.
src/components/__tests__/__snapshots__/Avatar.test.tsx.snap Updates Avatar snapshots.
example/src/Examples/ButtonExample.tsx Migrates trailing icons.
example/src/Examples/AvatarExample.tsx Migrates background colors.
example/src/Examples/AppbarExample.tsx Migrates Appbar background colors.
eslint.config.mjs Bans source StyleSheet.flatten usage.
docs/6.x/docs/guides/migration.md Documents breaking API changes.
Review details

Suppressed comments (3)

src/components/Appbar/Appbar.tsx:226

  • This changes the Appbar layout contract by adding top and bottom safe-area values to the computed height, but the Appbar tests do not assert the resulting height for either Appbar or Appbar.Header (including the custom header-height case described in the migration guide). Add a regression case covering the inset calculation so a future refactor cannot drop or double-count these values.
  // The safe area insets are applied as padding, so they need to be included in the height
  const height =
    modeAppbarHeight[mode] +
    (safeAreaInsets?.top ?? 0) +
    (safeAreaInsets?.bottom ?? 0);

src/components/Tooltip/Tooltip.tsx:201

  • The wrapper fallback only runs when the ref is null. A wrapped class or forwardRef component can yield a non-null custom instance that does not implement React Native's measure, so this calls an undefined method and the tooltip crashes instead of falling back as documented. Check that the ref exposes measure before selecting it; otherwise use childrenWrapperRef.current.
    const target = childRef.current ?? childrenWrapperRef.current;

    target?.measure((_x, _y, width, height, pageX, pageY) => {

src/components/Tooltip/Tooltip.tsx:108

  • The ref merge only reads children.props.ref, but React 18 stores an element ref on the element rather than in props. Since this package declares an unrestricted React peer, wrapping a child with a ref under React 18 will replace the user's ref with setChildRef and never invoke the original ref, contrary to the forwarding behavior documented by this change. Use a React-version-compatible ref merge and add coverage for the supported React 18 path.
  const childOwnRef = isValidChild
    ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      (children.props as TooltipChildProps).ref
    : undefined;

  // Keep a ref to the wrapped element so it can be measured directly,
  // while still forwarding the ref passed by the user (if any)
  const setChildRef = React.useCallback(
    (node: View | null) => {
      childRef.current = node;

      if (typeof childOwnRef === 'function') {
        childOwnRef(node);
      } else if (childOwnRef) {
        childOwnRef.current = node;
  • Files reviewed: 34/34 changed files
  • Comments generated: 4
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/components/Avatar/AvatarIcon.tsx
Comment thread src/components/Avatar/AvatarImage.tsx
Comment thread src/components/Avatar/AvatarText.tsx
Comment thread src/components/Appbar/Appbar.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants