Skip to content

feat(views): support React children in Nitro Views - #1644

Open
jkasprzyk17 wants to merge 5 commits into
margelo:mainfrom
jkasprzyk17:feat/hybrid-view-children
Open

feat(views): support React children in Nitro Views#1644
jkasprzyk17 wants to merge 5 commits into
margelo:mainfrom
jkasprzyk17:feat/hybrid-view-children

Conversation

@jkasprzyk17

Copy link
Copy Markdown

Closes #873

A Nitro View could not host React children. On Android React Native's mounting layer threw
IllegalStateException: Unable to add a view into a view that is not a ViewGroup and the app died;
on iOS the children mounted behind the native view, because RCTViewComponentView adds
contentView as its last subview while Fabric inserts children below it.

<GradientView colors={['#FF0080', '#7928CA']} style={{ padding: 24 }}>
  <Text>Welcome back</Text>
</GradientView>

API — opt in from the spec

export interface CardProps extends HybridViewProps {
  children?: HybridViewChildren
  isElevated: boolean
}
export type Card = HybridView<CardProps>

children is a marker, not a Nitro prop: nitrogen validates it and strips it before code
generation, so it never reaches the view config's validAttributes, the native props or the Hybrid
Object. It lives in the props interface because that is the only declaration site
getHostComponent<Props, Methods> can read — which is what makes the JSX guard below possible.

Android

The generated ViewManager becomes a ViewGroupManager<ViewGroup> — React Native's own abstraction,
so addView / getChildAt / getChildCount / removeViewAt / removeAllViews and
needsCustomLayoutForChildren() = false all come from the base class. No casts, no hand-written
child bookkeeping.

ViewGroupManager<T> requires T : ViewGroup (which is what stopped #1142), so the generated spec
narrows the view alongside it:

abstract class HybridCardSpec: HybridView() {
  abstract override val view: ViewGroup
}

Returning a leaf View from a children-enabled Nitro View is now a Kotlin compile error rather
than a runtime crash. Adds NitroViewGroup, a 25-line container with the same contract as
ReactViewGroup (measure to spec, no-op onLayout, no-op requestLayout) so Fabric keeps owning
layout.

iOS

The generated component mounts children into the Nitro view instead of alongside it, and pins that
view to the component's bounds — Yoga positions each child relative to the parent's border box, so
leaving contentView at getContentFrame() would inset every child by border + padding twice.
Both only for children-enabled Views, so leaf Views keep today's behaviour exactly.

Choosing a different container

view is not always where the children can go — UIVisualEffectView requires its contentView, a
third-party ViewGroup lays out its own children. Children-enabled Views get an overridable
childrenContainer that defaults to view:

class HybridBlurCard : HybridBlurCardSpec {
  private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
  var view: UIView { blurView }
  var childrenContainer: UIView { blurView.contentView }
}

Rejecting children

React Native throws at SurfaceMountingManager.addViewAt:285 before the ViewManager is ever
consulted, so an Android-side guard is unreachable. The check lives in shared C++ instead, in the
render phase, which is the one place both platforms pass through:

TestView cannot render React children! To render children inside this Nitro View,
declare a `children?: HybridViewChildren` prop in its Nitro spec.

Verified identical in logcat and os_log. Ahead of it, a View that did not opt in gets
children?: never, so passing children is a compile error.

Yoga

No trait changes. ConcreteViewShadowNode already lays children out correctly — a flex: 1 child
of a padding: 20 Nitro View measured x=20, width=80 on iOS before any fix.
ShadowNodeTraits::LeafYogaNode (suggested on the issue) would make it worse:
YogaLayoutableShadowNode::appendChild skips the Yoga child but the ShadowNode child still exists
and still gets mounted, so the Android crash stays and the children lose their layout entirely.

Backwards compatibility

Opt-in. Leaf Views keep SimpleViewManager<View> and regenerate byte-identically apart from one
line: ViewComponentDescriptor<…, false /* supportsChildren */>.

Testing

11 on-device tests in apps/example/__tests__/nitro.views.children.harness.tsx, green on an iPhone
17 Pro simulator (iOS 26.5) and an API 35 emulator — initial mount, z-order by pixel coverage,
padding/border/radius layout, margin, absolute positioning, clipping, add/remove/reorder/replace,
conditional mount, nested Nitro Views, childrenContainer, a 32-rerender stress loop, and a leaf
View regression. Plus four type-level tests verified by tsc --noEmit.

Full suites: 574/574 on Android, 573/574 on iOS (the one failure is the pre-existing
GC-timing-dependent createNativeArrayBuffer … external memory pressure test, which passes in
isolation).

Also adds a Children screen to the example app.

Known limitations

  • A container View's native view fills the component so the children land correctly, so an opaque
    native view paints over the component's own borderWidth/borderRadiusoverflow: 'hidden'
    clips it back.
  • overflow only reaches the native View on iOS; on Android it belongs to ReactViewGroup, so
    children of a Nitro View are never clipped there. Both are documented and pinned by a test.

Supersedes

#652, #1388 and #1444, and unblocks #1142. Compared to them this is opt-in (so no existing Nitro
View changes behaviour), uses ViewGroupManager instead of bolting IViewGroupManager onto
SimpleViewManager, moves the "not a ViewGroup" failure to compile time, replaces #1388's
unreachable runtime error with one that actually fires on both platforms, and ships tests.

Rendering React children inside a Nitro View crashed on Android with
`IllegalStateException: Unable to add a view into a view that is not a
ViewGroup`, and silently rendered them behind the native view on iOS.

A View now opts in by declaring a `children` prop of type
`HybridViewChildren` in its Nitro spec. `children` is a marker, not a Nitro
prop - it is stripped before code generation, so it never reaches the view
config or the native props.

- Android: the generated ViewManager becomes a `ViewGroupManager<ViewGroup>`
  and the generated spec narrows `view` to a `ViewGroup`, so a leaf `View`
  fails to compile instead of crashing at runtime. Adds `NitroViewGroup`, a
  container that leaves layout to Fabric.
- iOS: the generated component mounts children into the Nitro view itself
  (`contentView`) instead of alongside it, and pins that view to the
  component's bounds so Yoga's child frames are not inset twice.
- Views that did not opt in reject children in `ViewComponentDescriptor::
  appendChild` with the same actionable error on both platforms, and
  `children` is typed as `never` in JSX.

Leaf Views keep `SimpleViewManager<View>` and generate byte-identical
platform code apart from the new `supportsChildren` descriptor flag.

Closes margelo#873
React children are mounted into `view` by default, which does not work when the
native view manages its own subviews - `UIVisualEffectView` requires its
`contentView`, a third-party `ViewGroup` lays out its own children, and a native
map wants its markers in an overlay.

Children-enabled Views now expose an overridable `childrenContainer` that
defaults to `view`, so the simple case is unchanged. On Android the generated
ViewManager routes `addView`/`getChildAt`/`getChildCount`/`removeViewAt` through
it; on iOS the generated component resolves it once alongside `contentView` and
mounts children there.

Adds `ChildrenContainerTestView` to the test module, which mounts its children
into a sub-view, plus a harness test asserting the children land in the
container while `view` keeps exactly its own one child.
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

@jkasprzyk17 is attempting to deploy a commit to the Margelo Team on Vercel.

A member of the Team first needs to authorize it.

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.

[Feature] Support children in Nitro Views

1 participant