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..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, @@ -886,7 +892,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 +935,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( @@ -953,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 06aed3ab2c69..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 @@ -195,4 +198,69 @@ 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) + } + + /** 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() + } }