Skip to content

Custom TopBar button (react component) never resizes to content width under Fabric on iOS 26 and resets to 44pt after any button rebuild #8339

Description

@matyi

What happened?

On iOS 26 with the TopBar (RNNReactButtonView/RNNUIBarButtonItem's initWithCustomView: code path, gated behind if (@available(iOS 26.0, *))), a custom React component used as a topBar.rightButtons[].component never grows past the reserved 44pt minimum slot, even though the button is clearly designed to support it (see the kMinBarButtonSlotSize / "grow width once when React reports intrinsic size" comments in RNNUIBarButtonItem.mm and RNNReactButtonView.mm).

Image

Tracked this down to two separate, compounding bugs in the current implementation:

  1. The width-growth mechanism never fires under Fabric (New Architecture).
    The whole feature depends on -surface:didChangeIntrinsicSize: being called on RNNReactButtonView's delegate. That callback is only ever invoked by the legacy, non-Fabric RCTSurface (see RCTSurface.mm, setIntrinsicSize:[delegate surface:self didChangeIntrinsicSize:...], driven by rootShadowView:didChangeIntrinsicSize:). Under the New Architecture, RCTFabricSurface declares the same intrinsicSize property (inherited from RCTSurfaceProtocol) but never sets it and never calls the delegate method at all — there is no matching code path in RCTFabricSurface.mm. As a result, RNNReactButtonView.intrinsicSize/measuredSurfaceContentSize is always {0,0} and the button's width constraint is never updated from its initial 44pt value, regardless of how wide the mounted React content actually is.

  2. Even after working around (1), the button snaps back to 44pt the next time TopBar buttons are rebuilt (e.g. reselecting a bottom tab, or any mergeOptions/appear-triggered re-application of topBar.rightButtons). RNNButtonsPresenter.setButtons: calls RNNButtonBuilder.build:initWithCustomView: and constructs a brand new RNNUIBarButtonItem every time buttons are applied, even though the underlying RNNReactButtonView (looked up via RNNReactComponentRegistry.createComponentIfNotExists:) is reused (it's keyed by componentId in a map and only replaced/removed when actually different). initWithCustomView: unconditionally creates new UILayoutPriorityRequired width/height constraints on that reused view every single time, without ever deactivating the constraints created by the previous RNNUIBarButtonItem instance. This leaves multiple conflicting required-priority width constraints active on the same view (e.g. one pinned at the previously-grown width, a fresh one reset to 44pt) — Auto Layout can't satisfy both, and in practice the newly-added 44pt constraint wins, visibly snapping the button back down.

What was the expected behaviour?

A custom React component used as a TopBar button should size itself to its actual rendered content width (bounded below by the ~44pt minimum tap-target size), and should keep tracking that width correctly:

  • On initial mount, based on the component's real rendered content, not just the reserved 44pt placeholder.
  • On subsequent content changes within the same mounted component (e.g. a counter/badge whose text gets longer or shorter).
  • Consistently across repeated TopBar button re-applications (e.g. switching bottom tabs away and back), without the width getting reset or fighting with stale constraints from a previous RNNUIBarButtonItem instance.

Was it tested on latest react-native-navigation?

  • I have tested this issue on the latest react-native-navigation release and it still reproduces.

Help us reproduce this issue!

Reproduction steps

  1. Clone this public minimal reproduction repo:
    https://github.com/matyi/RNNGlassRepro

  2. Run the app and open the screen that uses a custom React component in topBar.rightButtons.

  3. Observe that the button renders variable-width content, but its width stays constrained and the content gets clipped instead of expanding to fit.

  4. As a workaround, manually measure the mounted view hierarchy and pass the actual size through handleIntrinsicSizeChange:. With that fallback in place, the button grows correctly on first mount.

  5. Put the screen inside a bottomTabs layout.

  6. Let the button grow, then switch to another tab and back, or trigger any flow that causes topBar.rightButtons to be re-applied, such as a mergeOptions call in componentDidAppear.

  7. Observe that the button width resets to the minimum size and clips the content again, even though the button content itself has not changed.

In what environment did this happen?

  • React Native Navigation version: 8.8.11
  • React Native version: 0.81.x (or even the lates 0.86.x)
  • Has Fabric (React Native's new rendering system) enabled: yes
  • Node version: v22.19.0
  • Device model: reproduced on iOS 26.1 simulator, also iphone 15 ios 26.5 real device
  • iOS version: 26.1, 26.5

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions