Skip to content

Commit 97cf220

Browse files
javachemeta-codesync[bot]
authored andcommitted
Remove tag from allocatedViewRegistry in destroyUnmountedShadowNode (#56274)
Summary: Pull Request resolved: #56274 When a ShadowNode is destroyed (e.g. from a superseded concurrent render or React Transition), destroyUnmountedShadowNode calls Java destroyUnmountedView which deletes the view from tagToViewState. However, the C++ allocatedViewRegistry was not updated, leaving a stale entry. This causes a crash when a subsequent executeMount encounters a Create mutation for the same tag: allocatedViewTags.contains(tag) returns true (stale entry from preallocate), so the Create mount item is skipped. But the Java view was already destroyed. When subsequent mount items (Insert, UpdateState, etc.) try to reference the tag, getViewState() throws RetryableMountingLayerException because the tag is not in tagToViewState. The fix mirrors what the normal Delete mutation path already does (line 638: allocatedViewTags.erase(tag)) - ensuring allocatedViewRegistry stays in sync with the Java-side tagToViewState. Changelog: [Android][Fixed] - Fix crash from stale preallocated view registry after concurrent render cancellation Reviewed By: lenaic Differential Revision: D98729251 fbshipit-source-id: 1a837138622b29bb62e31fb67aacb2c9ff4e9e5d
1 parent 2d78a39 commit 97cf220

2 files changed

Lines changed: 132 additions & 0 deletions

File tree

packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,20 @@ void FabricMountingManager::destroyUnmountedShadowNode(
10261026
auto tag = family.getTag();
10271027
auto surfaceId = family.getSurfaceId();
10281028

1029+
// Remove from allocatedViewRegistry so that executeMount does not skip
1030+
// the Create mount item for this tag. Without this, if the view was
1031+
// preallocated and then destroyed (e.g. due to a superseded concurrent
1032+
// render), executeMount would skip the Create because allocatedViewTags
1033+
// still contains the tag, but the Java side no longer has the view in
1034+
// tagToViewState (it was deleted by destroyUnmountedView below).
1035+
{
1036+
std::lock_guard allocatedViewsLock(allocatedViewsMutex_);
1037+
auto allocatedViewsIterator = allocatedViewRegistry_.find(surfaceId);
1038+
if (allocatedViewsIterator != allocatedViewRegistry_.end()) {
1039+
allocatedViewsIterator->second.erase(tag);
1040+
}
1041+
}
1042+
10291043
// ThreadScope::WithClassLoader is necessary because
10301044
// destroyUnmountedShadowNode is being called from a destructor thread
10311045
jni::ThreadScope::WithClassLoader([&]() {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
@file:Suppress("DEPRECATION")
9+
10+
package com.facebook.react.fabric
11+
12+
import com.facebook.react.ReactRootView
13+
import com.facebook.react.bridge.JavaOnlyMap
14+
import com.facebook.react.bridge.ReactTestHelper
15+
import com.facebook.react.fabric.mounting.MountingManager
16+
import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor
17+
import com.facebook.react.fabric.mounting.SurfaceMountingManager
18+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
19+
import com.facebook.react.uimanager.ThemedReactContext
20+
import com.facebook.react.uimanager.ViewManager
21+
import com.facebook.react.uimanager.ViewManagerRegistry
22+
import com.facebook.react.views.view.ReactViewManager
23+
import org.assertj.core.api.Assertions.assertThat
24+
import org.junit.Before
25+
import org.junit.Test
26+
import org.junit.runner.RunWith
27+
import org.robolectric.RobolectricTestRunner
28+
29+
/**
30+
* Tests for [SurfaceMountingManager] view lifecycle around preallocateView, deleteView, and
31+
* createView.
32+
*
33+
* The key scenario is the interaction between view preallocation and destroyUnmountedShadowNode
34+
* (D98729251): when a preallocated view is destroyed (e.g. from a superseded concurrent render),
35+
* and then a new Create mount item is emitted for the same tag, the view must be recreated.
36+
*
37+
* NOTE: These tests verify Java-side behavior only. The C++ fix in FabricMountingManager (erasing
38+
* from allocatedViewRegistry_ in destroyUnmountedShadowNode) ensures the Create mount item is
39+
* emitted in the first place. That behavior requires native code and is not testable via
40+
* Robolectric.
41+
*
42+
* See T223288217.
43+
*/
44+
@RunWith(RobolectricTestRunner::class)
45+
class SurfaceMountingManagerTest {
46+
private lateinit var mountingManager: MountingManager
47+
private lateinit var themedReactContext: ThemedReactContext
48+
private val surfaceId = 1
49+
50+
@Before
51+
fun setUp() {
52+
ReactNativeFeatureFlagsForTests.setUp()
53+
val reactContext = ReactTestHelper.createCatalystContextForTest()
54+
themedReactContext = ThemedReactContext(reactContext, reactContext, null, -1)
55+
mountingManager =
56+
MountingManager(
57+
ViewManagerRegistry(listOf<ViewManager<*, *>>(ReactViewManager())),
58+
MountItemExecutor {},
59+
)
60+
}
61+
62+
private fun startSurface(): SurfaceMountingManager {
63+
val rootView = ReactRootView(themedReactContext)
64+
mountingManager.startSurface(surfaceId, themedReactContext, rootView)
65+
return mountingManager.getSurfaceManagerEnforced(surfaceId, "test")
66+
}
67+
68+
@Test
69+
fun preallocateView_createsView() {
70+
val smm = startSurface()
71+
smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, true)
72+
assertThat(smm.getViewExists(42)).isTrue()
73+
}
74+
75+
@Test
76+
fun deleteView_removesPreallocatedView() {
77+
val smm = startSurface()
78+
smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, true)
79+
smm.deleteView(42)
80+
assertThat(smm.getViewExists(42)).isFalse()
81+
}
82+
83+
/**
84+
* Verifies the Java side of the scenario fixed by D98729251:
85+
* 1. preallocateView creates the view
86+
* 2. deleteView removes it (simulating destroyUnmountedShadowNode)
87+
* 3. createView recreates it (the C++ fix ensures this Create is actually emitted)
88+
* 4. addViewAt succeeds with the recreated view
89+
*/
90+
@Test
91+
fun createView_succeedsAfterPreallocateAndDelete() {
92+
val smm = startSurface()
93+
94+
smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, true)
95+
assertThat(smm.getViewExists(42)).isTrue()
96+
97+
smm.deleteView(42)
98+
assertThat(smm.getViewExists(42)).isFalse()
99+
100+
smm.createView("RCTView", 42, JavaOnlyMap.of(), null, null, true)
101+
assertThat(smm.getViewExists(42)).isTrue()
102+
103+
smm.addViewAt(surfaceId, 42, 0)
104+
assertThat(smm.getView(42)).isNotNull()
105+
}
106+
107+
/** createView is a no-op when the view already exists from preallocate (normal case). */
108+
@Test
109+
fun createView_isIdempotentWhenPreallocated() {
110+
val smm = startSurface()
111+
112+
smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, true)
113+
val viewBefore = smm.getView(42)
114+
115+
smm.createView("RCTView", 42, JavaOnlyMap.of(), null, null, true)
116+
assertThat(smm.getView(42)).isSameAs(viewBefore)
117+
}
118+
}

0 commit comments

Comments
 (0)