Description
Warning, the following text in this section is AI generated based on a session of me debugging this problem with GPT-5.4 in OpenCode, but the problem is very real & the proposed fix does work for me. Feel free to close, ignore or just tell me "go away" if you don't want issues like this, I understand it's a burden on maintainers to have to read through what might be slop.
Summary
On iOS, InterceptingGestureDetector / RNGestureHandlerDetectorShadowNode can crash during layout with:
react_native_assert(YGNodeGetOwner(childYogaNode) == &yogaNode_);
from YogaLayoutableShadowNode::layout.
The problem appears to be in RNGestureHandlerDetectorShadowNode::unflattenNode(...), which currently clones detector children with node->clone({}).
Crash
The native stack looks like:
facebook::react::YogaLayoutableShadowNode::layout
facebook::react::RNGestureHandlerDetectorShadowNode::layout
facebook::react::YogaLayoutableShadowNode::layout
...
The assert is in React Native here:
// YogaLayoutableShadowNode.cpp
react_native_assert(YGNodeGetOwner(childYogaNode) == &yogaNode_);
Root Cause
RNGestureHandlerDetectorShadowNode does custom child cloning/unflattening:
appendChild(...) calls unflattenNode(child)
replaceChild(...) calls unflattenNode(newChild)
unflattenNode(...) does node->clone({})
The issue is that clone({}) interacts badly with RN Fabric's YogaLayoutableShadowNode clone semantics:
- RN
clone ctor copies the source yoga node.
- It calls
updateYogaChildrenOwnersIfNeeded(), which invalidates ownership of direct yoga children on the clone.
- Because the fragment passed to
clone(...) has no explicit children, the clone also inherits yogaTreeHasBeenConfigured_ from the source node.
- Later, Fabric may skip reconfiguring that cloned subtree in
configureYogaTree() because it appears "already configured".
- That means the cloned node's direct yoga children can still have stale/non-parent owners when layout runs.
YogaLayoutableShadowNode::layout() then asserts when it sees a child yoga node that is not owned by the detector yoga node.
So the problem is not just "layout churn" or app-specific structure. The detector is creating a cloned subtree whose descendant yoga ownership has been invalidated, while also preserving enough configuration state for RN to skip the repair path.
Relevant RNGH code
Current code in RNGestureHandlerDetectorShadowNode.cpp:
std::shared_ptr<const ShadowNode>
RNGestureHandlerDetectorShadowNode::unflattenNode(
const std::shared_ptr<const ShadowNode> &node) {
auto clonedNode = node->clone({});
auto clonedNodeWithProtectedAccess =
std::static_pointer_cast<RNGestureHandlerDetectorShadowNode>(clonedNode);
clonedNodeWithProtectedAccess->traits_.set(ShadowNodeTraits::FormsView);
clonedNodeWithProtectedAccess->traits_.set(
ShadowNodeTraits::FormsStackingContext);
return clonedNode;
}
Local Fix That Stops The Crash
Changing unflattenNode(...) to clone with an explicit children fragment fixes the crash locally for me:
std::shared_ptr<const ShadowNode>
RNGestureHandlerDetectorShadowNode::unflattenNode(
const std::shared_ptr<const ShadowNode> &node) {
auto clonedNode = node->clone({
.props = ShadowNodeFragment::propsPlaceholder(),
.children =
std::make_shared<const std::vector<std::shared_ptr<const ShadowNode>>>(
node->getChildren()),
.state = ShadowNodeFragment::statePlaceholder(),
});
auto clonedNodeWithProtectedAccess =
std::static_pointer_cast<RNGestureHandlerDetectorShadowNode>(clonedNode);
clonedNodeWithProtectedAccess->traits_.set(ShadowNodeTraits::FormsView);
clonedNodeWithProtectedAccess->traits_.set(
ShadowNodeTraits::FormsStackingContext);
return clonedNode;
}
My understanding is that this works because passing a real children fragment forces RN to rebuild the cloned subtree's yoga children instead of preserving stale configured state from the source clone.
Why I Think This Is The Correct Direction
This change:
- fixes the crash locally
- preserves gesture behaviour
- directly targets the ownership/configuration mismatch between RNGH's synthetic child clone and RN Fabric's yoga ownership model
Extra Note
There is also another suspicious implementation detail in this file: in layout(...), arbitrary children are static_pointer_cast to RNGestureHandlerDetectorShadowNode just to access protected members:
auto childWithProtectedAccess =
std::static_pointer_cast<const RNGestureHandlerDetectorShadowNode>(
yogaChild);
That seems unsafe / UB if the child is not actually a detector node, though I do not think that is the direct cause of this specific assert.
Steps to reproduce
This bit is me, not AI:
It's really difficult for me to replicate this in something I can share, I only see it in my real project which is complex & not open source. The only hint I can give is that I have structure like this View A ( View B ( [many more nested] ( InterceptingGestureDetector ( ... )))) with a useLayoutEffect containing a measure callback on a ref to B that then sets state to set height on A. Weird setup, I know, but I need to round sizes to pixel boundaries. Removing that resize stops the crash. But I can't get it to crash in a smaller setup, although I admit I haven't spent hours on it.
A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
sorry, I really can't work this out in a small setup.
Gesture Handler version
3.0.0-nightly-20260331-9d671cb78
React Native version
0.83.4
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
iPhone 15 Pro Max, iOS 26.4
Acknowledgements
Yes
Description
Warning, the following text in this section is AI generated based on a session of me debugging this problem with GPT-5.4 in OpenCode, but the problem is very real & the proposed fix does work for me. Feel free to close, ignore or just tell me "go away" if you don't want issues like this, I understand it's a burden on maintainers to have to read through what might be slop.
Summary
On iOS,
InterceptingGestureDetector/RNGestureHandlerDetectorShadowNodecan crash during layout with:react_native_assert(YGNodeGetOwner(childYogaNode) == &yogaNode_);from
YogaLayoutableShadowNode::layout.The problem appears to be in
RNGestureHandlerDetectorShadowNode::unflattenNode(...), which currently clones detector children withnode->clone({}).Crash
The native stack looks like:
The assert is in React Native here:
Root Cause
RNGestureHandlerDetectorShadowNodedoes custom child cloning/unflattening:appendChild(...)callsunflattenNode(child)replaceChild(...)callsunflattenNode(newChild)unflattenNode(...)doesnode->clone({})The issue is that
clone({})interacts badly with RN Fabric'sYogaLayoutableShadowNodeclone semantics:clonector copies the source yoga node.updateYogaChildrenOwnersIfNeeded(), which invalidates ownership of direct yoga children on the clone.clone(...)has no explicit children, the clone also inheritsyogaTreeHasBeenConfigured_from the source node.configureYogaTree()because it appears "already configured".YogaLayoutableShadowNode::layout()then asserts when it sees a child yoga node that is not owned by the detector yoga node.So the problem is not just "layout churn" or app-specific structure. The detector is creating a cloned subtree whose descendant yoga ownership has been invalidated, while also preserving enough configuration state for RN to skip the repair path.
Relevant RNGH code
Current code in
RNGestureHandlerDetectorShadowNode.cpp:Local Fix That Stops The Crash
Changing
unflattenNode(...)to clone with an explicit children fragment fixes the crash locally for me:My understanding is that this works because passing a real children fragment forces RN to rebuild the cloned subtree's yoga children instead of preserving stale configured state from the source clone.
Why I Think This Is The Correct Direction
This change:
Extra Note
There is also another suspicious implementation detail in this file: in
layout(...), arbitrary children arestatic_pointer_casttoRNGestureHandlerDetectorShadowNodejust to access protected members:That seems unsafe / UB if the child is not actually a detector node, though I do not think that is the direct cause of this specific assert.
Steps to reproduce
This bit is me, not AI:
It's really difficult for me to replicate this in something I can share, I only see it in my real project which is complex & not open source. The only hint I can give is that I have structure like this
View A ( View B ( [many more nested] ( InterceptingGestureDetector ( ... ))))with auseLayoutEffectcontaining ameasurecallback on a ref to B that then sets state to set height on A. Weird setup, I know, but I need to round sizes to pixel boundaries. Removing that resize stops the crash. But I can't get it to crash in a smaller setup, although I admit I haven't spent hours on it.A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
sorry, I really can't work this out in a small setup.
Gesture Handler version
3.0.0-nightly-20260331-9d671cb78
React Native version
0.83.4
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
iPhone 15 Pro Max, iOS 26.4
Acknowledgements
Yes