Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions patches/react-native/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,11 @@
- Upstream PR/issue: https://github.com/react/react-native/pull/57546 (merged as `06eb1fe`)
- E/App issue: https://github.com/Expensify/App/issues/97127
- PR introducing patch: https://github.com/Expensify/App/pull/98095

### [react-native+0.86.0+042+fix-unmount-assert-container-view.patch](react-native+0.86.0+042+fix-unmount-assert-container-view.patch)

- Reason: `-[RCTViewComponentView unmountChildComponentView:index:]` asserts `childComponentView.superview == self.currentContainerView`, but `currentContainerView` is not a pure getter. When `_useCustomContainerView` flips it creates or tears down `_containerView` and moves the existing subviews across, so reading it mutates the very hierarchy the assertion is inspecting. The order in which the two operands of `==` are evaluated is unspecified, so `superview` can be read before the migration runs and the check then compares a pre-migration superview against the post-migration container view. The `[childComponentView removeFromSuperview]` immediately afterwards is unaffected, which is why this only ever surfaces as an assertion failure and never as broken rendering. `_useCustomContainerView` is recomputed on every `finalizeUpdates` from `styleWouldClipOverflowInk`, so any view whose style crosses that threshold while it has mounted children is exposed. The patch resolves the container view into a local before the assertions read `superview`.
- Symptom: `SIGABRT`, `NSInternalInconsistencyException: Attempt to unmount a view which is mounted inside a different view.` The reported `existing parent` tag equals the parent's own tag, which is the tell that the child really is inside the parent, just not inside the parent's container view. `RCTAssert` compiles out where `NS_BLOCK_ASSERTIONS` is set, so only builds that keep assertions on abort here.
- Upstream PR/issue: This should ideally be fixed upstream, but no PR has been filed yet.
- E/App issue: [#100008](https://github.com/Expensify/App/issues/100008) [#98970](https://github.com/Expensify/App/issues/98970) — first seen while bulk-duplicating reports on iOS, but not specific to that flow and it reproduces on `main`
- PR introducing patch: https://github.com/Expensify/App/pull/100016
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
index 22c6a3c..eeaa8ff 100644
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
@@ -175,6 +175,12 @@ static BOOL RCTLayerTransformCollapsesAxis(CALayer *layer)
if (_removeClippedSubviews) {
[_reactSubviews removeObjectAtIndex:index];
} else {
+ // `currentContainerView` is not a pure getter: it moves the existing subviews across whenever the custom
+ // container view is created or torn down. Resolve it once, before `superview` is read, so every assertion below
+ // compares state from the same side of that migration. Left as `self.currentContainerView` the `==` operands may
+ // be evaluated in either order, and a pre-migration `superview` then gets compared against the post-migration
+ // container view.
+ UIView *containerView = self.currentContainerView;
RCTAssert(
childComponentView.superview != nil,
@"Attempt to unmount a view which is not mounted. (parent: %@, child: %@, index: %@)",
@@ -182,21 +188,20 @@ static BOOL RCTLayerTransformCollapsesAxis(CALayer *layer)
childComponentView,
@(index));
RCTAssert(
- childComponentView.superview == self.currentContainerView,
+ childComponentView.superview == containerView,
@"Attempt to unmount a view which is mounted inside a different view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));
RCTAssert(
- (self.currentContainerView.subviews.count > index) &&
- [self.currentContainerView.subviews objectAtIndex:index] == childComponentView,
+ (containerView.subviews.count > index) && [containerView.subviews objectAtIndex:index] == childComponentView,
@"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
self,
childComponentView,
@(index),
- @([self.currentContainerView.subviews indexOfObject:childComponentView]),
- @([[self.currentContainerView.subviews objectAtIndex:index] tag]));
+ @([containerView.subviews indexOfObject:childComponentView]),
+ @([[containerView.subviews objectAtIndex:index] tag]));
}

[childComponentView removeFromSuperview];
Loading