From 326373189e5d3cac82eb54fe2b8bf5ac6a633fb6 Mon Sep 17 00:00:00 2001 From: VimukthiShohan Date: Tue, 3 Nov 2026 12:24:35 +0530 Subject: [PATCH 1/2] fix(android): soft-fail missing view in updateOverflowInset and updatePadding A ViewState can exist without an Android View (createViewUnsafe only creates one for layoutable nodes, and updateEventEmitter registers bare placeholders). updateOverflowInset and updatePadding then hit checkNotNull and throw "Unable to find View for tag", tearing down the whole surface. Log a ReactNoCrashSoftException and return instead, matching the missing-viewState branch right above each assert. Adds Robolectric tests for both methods. --- .../fabric/mounting/SurfaceMountingManager.kt | 19 +++++++++++-- .../fabric/SurfaceMountingManagerTest.kt | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 0c8c7bf05502..719f86d3c718 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -886,7 +886,14 @@ internal constructor( return } - val viewToUpdate = checkNotNull(viewState.view) { "Unable to find View for tag: $reactTag" } + val viewToUpdate = viewState.view + if (viewToUpdate == null) { + ReactSoftExceptionLogger.logSoftException( + ReactSoftExceptionLogger.Categories.SURFACE_MOUNTING_MANAGER_MISSING_VIEWSTATE, + ReactNoCrashSoftException("ViewState has no view for tag: [$reactTag] for updatePadding"), + ) + return + } val viewManager = checkNotNull(viewState.viewManager) { "Unable to find ViewManager for view: $viewState" } @@ -922,7 +929,15 @@ internal constructor( } val viewToUpdate = viewState.view - checkNotNull(viewToUpdate) { "Unable to find View for tag: $reactTag" } + if (viewToUpdate == null) { + ReactSoftExceptionLogger.logSoftException( + ReactSoftExceptionLogger.Categories.SURFACE_MOUNTING_MANAGER_MISSING_VIEWSTATE, + ReactNoCrashSoftException( + "ViewState has no view for tag: [$reactTag] for updateOverflowInset", + ), + ) + return + } if (viewToUpdate is ReactOverflowViewWithInset) { (viewToUpdate as ReactOverflowViewWithInset).setOverflowInset( diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt index 06aed3ab2c69..20f7fd52eaf7 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt @@ -195,4 +195,32 @@ class SurfaceMountingManagerTest { smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, true) assertThat(smm.getViewExists(42)).isTrue() } + + /** + * A ViewState can exist without an Android View (createViewUnsafe only creates one when the node + * is layoutable). updateOverflowInset on such a tag used to throw and tear down the surface; it + * should log a soft exception and return, as the missing-viewState branch already does. + */ + @Test + fun updateOverflowInset_doesNotThrowWhenViewStateHasNoView() { + val smm = startSurface() + + // isLayoutable = false: a ViewState is registered, but no Android View is created for it. + smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, false) + assertThat(smm.getViewExists(42)).isTrue() + + smm.updateOverflowInset(42, 0, 0, 0, 0) + } + + /** Same as above, for updatePadding. */ + @Test + fun updatePadding_doesNotThrowWhenViewStateHasNoView() { + val smm = startSurface() + + // isLayoutable = false: a ViewState is registered, but no Android View is created for it. + smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, false) + assertThat(smm.getViewExists(42)).isTrue() + + smm.updatePadding(42, 0, 0, 0, 0) + } } From d447fd6535da3872def3be63c71ef670d3e724db Mon Sep 17 00:00:00 2001 From: VimukthiShohan Date: Tue, 3 Nov 2026 13:03:41 +0530 Subject: [PATCH 2/2] fix(android): soft-fail missing view in removeViewAt and updateState Same ViewState-without-View case as updateOverflowInset and updatePadding. removeViewAt hit checkNotNull on the parent view ("Unable to find parentView for tag") and updateState on the ViewManager, both throwing and tearing down the surface. Log a ReactNoCrashSoftException and return instead. updateState keeps storing the new state wrapper and still destroys the previous one on the early return, so the C++ State is freed immediately. Adds Robolectric tests for both methods, plus one that verifies the replaced state wrapper is destroyed on the updateState early return. --- .../fabric/mounting/SurfaceMountingManager.kt | 21 ++++++++-- .../fabric/SurfaceMountingManagerTest.kt | 40 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 719f86d3c718..bc554d1dfd49 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -425,7 +425,13 @@ internal constructor( } val parentView = parentViewState.view - checkNotNull(parentView) { "Unable to find parentView for tag $parentTag" } + if (parentView == null) { + ReactSoftExceptionLogger.logSoftException( + ReactSoftExceptionLogger.Categories.SURFACE_MOUNTING_MANAGER_MISSING_VIEWSTATE, + ReactNoCrashSoftException("ViewState has no view for tag: [$parentTag] for removeViewAt"), + ) + return + } if (parentView !is ViewGroup) { ReactSoftExceptionLogger.logSoftException( TAG, @@ -968,9 +974,16 @@ internal constructor( val prevStateWrapper = viewState.stateWrapper viewState.stateWrapper = stateWrapper - val viewManager = - checkNotNull(viewState.viewManager) { "Unable to find ViewManager for tag: $reactTag" } - val view = checkNotNull(viewState.view) + val viewManager = viewState.viewManager + val view = viewState.view + if (viewManager == null || view == null) { + ReactSoftExceptionLogger.logSoftException( + ReactSoftExceptionLogger.Categories.SURFACE_MOUNTING_MANAGER_MISSING_VIEWSTATE, + ReactNoCrashSoftException("ViewState has no view for tag: [$reactTag] for updateState"), + ) + prevStateWrapper?.destroyState() + return + } val extraData = viewManager.updateState(view, viewState.currentProps, stateWrapper) if (extraData != null) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt index 20f7fd52eaf7..71cbd7362add 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerTest.kt @@ -16,6 +16,7 @@ import com.facebook.react.fabric.mounting.MountingManager import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor import com.facebook.react.fabric.mounting.SurfaceMountingManager import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManagerRegistry @@ -25,6 +26,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -223,4 +226,41 @@ class SurfaceMountingManagerTest { smm.updatePadding(42, 0, 0, 0, 0) } + + /** Same as above, for removeViewAt when the parent's ViewState has no View. */ + @Test + fun removeViewAt_doesNotThrowWhenParentViewStateHasNoView() { + val smm = startSurface() + + // isLayoutable = false: a ViewState is registered, but no Android View is created for it. + smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, false) + assertThat(smm.getViewExists(42)).isTrue() + + smm.removeViewAt(43, 42, 0) + } + + /** Same as above, for updateState. */ + @Test + fun updateState_doesNotThrowWhenViewStateHasNoView() { + val smm = startSurface() + + // isLayoutable = false: a ViewState is registered, but no View or ViewManager is set on it. + smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, false) + assertThat(smm.getViewExists(42)).isTrue() + + smm.updateState(42, null) + } + + /** The early return in updateState must still free the replaced state wrapper. */ + @Test + fun updateState_destroysPreviousStateWhenViewStateHasNoView() { + val smm = startSurface() + smm.preallocateView("RCTView", 42, JavaOnlyMap.of(), null, false) + + val first = mock() + smm.updateState(42, first) + smm.updateState(42, mock()) + + verify(first).destroyState() + } }