feat(views): support React children in Nitro Views - #1644
Open
jkasprzyk17 wants to merge 5 commits into
Open
Conversation
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.
|
@jkasprzyk17 is attempting to deploy a commit to the Margelo Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ViewGroupand the app died;on iOS the children mounted behind the native view, because
RCTViewComponentViewaddscontentViewas its last subview while Fabric inserts children below it.API — opt in from the spec
childrenis a marker, not a Nitro prop: nitrogen validates it and strips it before codegeneration, so it never reaches the view config's
validAttributes, the native props or the HybridObject. 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/removeAllViewsandneedsCustomLayoutForChildren() = falseall come from the base class. No casts, no hand-writtenchild bookkeeping.
ViewGroupManager<T>requiresT : ViewGroup(which is what stopped #1142), so the generated specnarrows the view alongside it:
Returning a leaf
Viewfrom a children-enabled Nitro View is now a Kotlin compile error ratherthan a runtime crash. Adds
NitroViewGroup, a 25-line container with the same contract asReactViewGroup(measure to spec, no-oponLayout, no-oprequestLayout) so Fabric keeps owninglayout.
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
contentViewatgetContentFrame()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
viewis not always where the children can go —UIVisualEffectViewrequires itscontentView, athird-party
ViewGrouplays out its own children. Children-enabled Views get an overridablechildrenContainerthat defaults toview:Rejecting children
React Native throws at
SurfaceMountingManager.addViewAt:285before the ViewManager is everconsulted, 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:
Verified identical in logcat and
os_log. Ahead of it, a View that did not opt in getschildren?: never, so passing children is a compile error.Yoga
No trait changes.
ConcreteViewShadowNodealready lays children out correctly — aflex: 1childof a
padding: 20Nitro View measuredx=20, width=80on iOS before any fix.ShadowNodeTraits::LeafYogaNode(suggested on the issue) would make it worse:YogaLayoutableShadowNode::appendChildskips the Yoga child but the ShadowNode child still existsand 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 oneline:
ViewComponentDescriptor<…, false /* supportsChildren */>.Testing
11 on-device tests in
apps/example/__tests__/nitro.views.children.harness.tsx, green on an iPhone17 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 leafView 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 pressuretest, which passes inisolation).
Also adds a Children screen to the example app.
Known limitations
native view paints over the component's own
borderWidth/borderRadius—overflow: 'hidden'clips it back.
overflowonly reaches the native View on iOS; on Android it belongs toReactViewGroup, sochildren 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
ViewGroupManagerinstead of boltingIViewGroupManagerontoSimpleViewManager, moves the "not a ViewGroup" failure to compile time, replaces #1388'sunreachable runtime error with one that actually fires on both platforms, and ships tests.