software-mansion / software-mansion/react-native-screens

Fabric/Android: views re-parented inside a screen crash with "View already has a parent" while its removal transition is active

Open
#4,677 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

cr:missing-info cr:missing-repro cr:platform:android missing-info missing-repro platform:android
Dominant language
TypeScript
Stars
3.7k
Forks
714
Avg merge
2d 23h
Merged PRs (30d)
71

Description

Description

On Fabric/Android, any view that Fabric re-parents inside a screen while that screen's removal transition is active crashes the app with addViewAt: ... View already has a parent. It is not limited to reorders, and it does not need removeClippedSubviews.

Screen.startTransitionRecursive marks the whole outgoing subtree with ViewGroup.startViewTransition (Screen.kt, via screen.post { startRemovalTransition() } in NativeProxy.kt). AOSP's ViewGroup.removeFromArray then deliberately keeps mParent set for views in mTransitioningViews. So when Fabric's mount batch removes such a view and re-inserts it under a different parent, the insert throws — and React Native's own recovery in SurfaceMountingManager.addViewAt calls removeView(), which is a no-op once the view is already out of the child array.

What we hit

Navigating replace() from one onboarding step to the next. The outgoing screen's CTA leaves its loading state in the same frame: the button's content View loses { opacity: 0 }, Fabric therefore flattens it, and the label Text is re-parented one level up to the button root. That move is the thing that explodes.

java.lang.IllegalStateException: addViewAt: cannot insert view [1604] into parent [1608]:
  View already has a parent: [1606]  Parent: ReactViewGroup  View: ReactTextView
    at com.facebook.react.fabric.mounting.SurfaceMountingManager.addViewAt(SurfaceMountingManager.kt:343)

java.lang.IllegalStateException: addViewAt: failed to insert view [1604] into parent [1608] at index 0
    at com.facebook.react.fabric.mounting.SurfaceMountingManager.addViewAt(SurfaceMountingManager.kt:372)
    at com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem.execute(IntBufferBatchMountItem.kt:122)
    at com.facebook.react.fabric.mounting.MountItemDispatcher.dispatchMountItems(MountItemDispatcher.kt:250)
    at com.facebook.react.fabric.FabricUIManager$DispatchUIFrameCallback.doFrameGuarded(FabricUIManager.java:1622)
Caused by: java.lang.IllegalStateException: The specified child already has a parent.
    at android.view.ViewGroup.addViewInner(ViewGroup.java:5333)
    at com.facebook.react.views.view.ReactClippingViewManager.addView(ReactClippingViewManager.kt:36)

The tree from SurfaceMountingManager's own dump, inside the screen being removed:

1608 RCTView              <- button root (target parent)
 |- 1606 RCTView          <- current parent of 1604
 |   \- 1604 RCTText      <- the label being moved up
 \- 1664 AndroidProgressBar

100% reproducible for us, debug and release. iOS is unaffected — RNSScreen.mm preserves dismissed content with snapshotViewAfterScreenUpdates, so nothing is retained.

Why the existing PRs do not cover it
  • #4492 detects a screen being moved and skips the removal transition. Ours is a genuine removal (replace() really does remove the screen), so that condition would not fire.
  • #3249 / #3250 describe the same retention: endTransitionRecursive iterates parent.children, so a view Fabric removed during the transition never receives endViewTransition and stays marked.
  • facebook/react-native#47634 fixed this AOSP behaviour only for the removeClippedSubviews path. Here ReactClippingViewManager.addView:36 is the plain parent.addView(child, index) branch.
Environment
  • react-native-screens 4.28.0
  • react-native 0.87.1, New Architecture (Fabric + bridgeless), Hermes
  • react-native-reanimated 4.6.0 — ruled out: removing the screen's entering/exiting layout animations, and replacing Animated.View with plain View, changes nothing
  • Xiaomi 13, Android 16
Workaround

Pinning the moved view with collapsable={false} so it can never be flattened, and therefore never moved, removes the crash. That only covers the flattening trigger — a sibling reorder inside a removing screen would still hit it.

Possible directions

Either end the transition for a view at the moment it is removed while marked (endViewTransition, which clears mTransitioningViews/mParent), or avoid marking descendants that React may still mutate during the animation.

Honest caveats

We do not have a minimal reproduction repository — this comes from a production app, so the report is the mechanism plus the evidence above rather than a runnable sample. Happy to test a patch against our case, which reproduces every time.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing removal-transition handling from screen.post { startRemovalTransition() } in NativeProxy.kt through Screen.kt, then inspect SurfaceMountingManager.kt and ReactClippingViewManager.kt around addViewAt. Done means the reported Fabric/Android re-parenting during an active removal transition no longer crashes, including the described non-clipping case; the report has no minimal reproduction or named test.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin, react-native
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.