react / react/react-native

[Android][Fabric] Create mutation missing from an unflatten reparenting batch under remount churn — production evidence for the #47960 class (surface-killing)

Open
#58,265 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Needs: Author Feedback Needs: Repro
Dominant language
C++
Stars
127k
Forks
25.3k
Avg merge
1d 23h
Merged PRs (30d)
4

Description

Framing: this appears to be a concrete, reproducible, production-scale instance of #47960 ("Transaction merging can cause CREATE mutations to be effectively dropped", closed as not_planned) — filed with fresh evidence because the impact is a dead surface at ~2% of Android sessions, the graceful-handling attempts (#56654, #56389, #56376) are unmerged, and the differentiator-side fixes that DID land (LIS differ, parentTag fixes, present in 0.85.3) do not prevent it.

Summary

When a flattened (layout-only) View must be unflattened in a commit that races with remount churn of the same subtree, the emitted IntBufferBatchMountItem can contain the reparenting sequence for the materialized wrapper without its Create mutation. SurfaceMountingManager.addViewAt then throws RetryableMountingLayerException: Unable to find viewState for tag, and since batch items are not retried (only view commands are), MountItemDispatcher.dispatchMountItems rethrows and the host tears the instance down — on a production app this is a dead white screen (or a hard crash under expo-updates error recovery).

Evidence

Captured on device (RN 0.85.3, bridgeless, Galaxy S23 Ultra, Android 16). The failing batch, printed by MountItemDispatcher:

REMOVE [2180]->[14] @3     // existing child removed from its parent...
INSERT [2184]->[14] @3     // ...a NEW view takes its slot — but 2184 has no viewState:
INSERT [2180]->[2184] @0   //    no Create/preallocation anywhere for it
UPDATE PROPS [2180]
UPDATE STATE [2180]
UPDATE LAYOUT [2184]->[14]: x:0 y:0 w:1080 h:2316
UPDATE LAYOUT [2180]->[2184]: x:0 y:0 w:1080 h:2316
UPDATE EVENTEMITTER [2184]
UPDATE EVENTEMITTER [2180]

printSurfaceState at crash time confirms tag 2184 is absent from the registry (526 views listed; 2184 not among them), i.e. the Create was never emitted or executed — this is not an ordering problem within the batch.

The shape is the classic unflatten sequence: child keeps its tag and reparents into the newly materialized wrapper. The wrapper here is a library-owned layout-only view (StyleSheet.absoluteFill + pointerEvents="box-none") around a native sheet component, whose sibling/outer wrapper toggles pointerEvents between 'none' and 'auto' — the prop change that makes it non-flattenable.

Causal confirmation: forcing the outermost wrapper real with collapsable={false} moved the identical missing-Create crash exactly one level down to the next flattenable wrapper in the ancestor chain (REMOVE [3010]->[3016]; INSERT [3014]->[3016]; INSERT [3010]->[3014] — 3016 now materialized and registered, 3014 missing). Pinning every wrapper in the chain eliminates the crash entirely (0/40 reproductions vs ~1/3 before).

Trigger conditions (from ~150 production events + device repro)

  • Android, New Architecture (Fabric), RN 0.85.3 (production telemetry also on 0.85.x; the sub-signature first appeared for us after adopting native-stack screens, and scaled ~20x when a high-frequency bottom-sheet moved onto this wrapper shape).
  • A flattenable wrapper whose subtree is rapidly remounted (portal entry torn down and recreated within a frame or two — e.g. reopening a sheet before the previous instance finished closing) while a prop change forces the wrapper to unflatten.
  • Reproduces within ~3 attempts on device with that churn; the same UI used slowly does not reproduce.

Where the Create is likely lost

The differentiator emitting Insert-without-Create seems unlikely given the unflatten fixes already present in 0.85.3 (LIS differ #56094, parentTag fixes #50884/#56542 — all included, crash persists). The shape instead matches transaction merging (#44188 / #47960): the unflatten commit's Create lands in one transaction, the churn (portal entry unmount/remount within a frame or two, main thread busy with the sheet teardown) produces adjacent transactions, and the merge drops the Create while keeping the Insert/reparent. We have not instrumented MountingCoordinator::pullTransaction to prove the merge directly, but the trigger profile (only under rapid remount churn; never when the same UI is used slowly) is consistent with it.

Repro

Minimal standalone repro not yet extracted; the real-world trigger is @swmansion/react-native-bottom-sheet's portal host (issue filed there with the same evidence: software-mansion-labs/react-native-bottom-sheet#78, one-line fix in software-mansion-labs/react-native-bottom-sheet#79), where each portal entry is <View style={absoluteFill} pointerEvents="box-none"> around a native sheet, remounted under user churn. On a Galaxy S23 Ultra this reproduced within ~3 open/act/reopen cycles. Happy to work toward a minimal repro if the mutation traces above are not sufficient to locate the path.

Expected

A batch must never contain an Insert for a tag that no executed transaction has Created — regardless of transaction merging or concurrent remount churn. Secondarily: a missing viewState in the batch-mount path destroys the entire React instance in bridgeless mode (white screen, app unusable); the graceful-handling direction of #56654 would at least bound the blast radius.

Workaround

collapsable={false} on every flattenable ancestor wrapper of the affected subtree (prevents the flatten→unflatten transition entirely).

Contributor guide

Open the contributing guide

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 at MountingCoordinator::pullTransaction and trace transaction merging into MountItemDispatcher.dispatchMountItems and SurfaceMountingManager.addViewAt, using the captured batch sequence as the failing case. Reproduce the flatten/unflatten remount churn on Android; done means no batch inserts an uncreated tag and a missing viewState does not tear down the instance. A minimal standalone repro is not yet available.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, cpp, react-native
Domain
mobile
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.