Skip to content

Commit b66529a

Browse files
javachefacebook-github-bot
authored andcommitted
Unmount React applications when re-creating context [RFC]
Summary: Currently in RN Android when re-creating the context we don't unmount the underlying React application. This violates ViewManager and React hooks contracts, who are no longer able to properly unmount views, and instead the view hierarchy is forcefully torn down by Android UI. This differs from iOS, where we do unmount the application on reloads. This is a trade-off with performance, as we'll keep the JS thread alive slightly longer to complete shutdown, but it's the right call for correctness. It also only mainly affects development, as recreating the context is rare in production. Repro steps: ``` useEffect(() => { console.log('Playground useEffect invoked'); return () => { console.log('Playground useEffect destructor invoked'); }; }, []); ``` Validate that when reloading the application, the second console.log is printed. Changelog: [Android][Changed] React trees will be unmounted when the application is reloaded Differential Revision: D45145520 fbshipit-source-id: 6800bba918b5d0a8cfac3a835eda0b397f47e480
1 parent 2d9c817 commit b66529a

File tree

3 files changed

+61
-84
lines changed

3 files changed

+61
-84
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@
9292
import com.facebook.react.modules.core.DeviceEventManagerModule;
9393
import com.facebook.react.modules.core.ReactChoreographer;
9494
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
95-
import com.facebook.react.modules.fabric.ReactFabric;
9695
import com.facebook.react.packagerconnection.RequestHandler;
9796
import com.facebook.react.surface.ReactStage;
9897
import com.facebook.react.turbomodule.core.TurboModuleManager;
@@ -102,6 +101,7 @@
102101
import com.facebook.react.uimanager.ReactRoot;
103102
import com.facebook.react.uimanager.UIManagerHelper;
104103
import com.facebook.react.uimanager.ViewManager;
104+
import com.facebook.react.uimanager.common.UIManagerType;
105105
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
106106
import com.facebook.soloader.SoLoader;
107107
import com.facebook.systrace.Systrace;
@@ -855,7 +855,10 @@ private void clearReactRoot(ReactRoot reactRoot) {
855855
* with the provided reactRoot reactRoot will be started asynchronously, i.e this method won't
856856
* block. This reactRoot will then be tracked by this manager and in case of catalyst instance
857857
* restart it will be re-attached.
858+
*
859+
* @deprecated This method should be internal to ReactRootView and ReactInstanceManager
858860
*/
861+
@Deprecated
859862
@ThreadConfined(UI)
860863
public void attachRootView(ReactRoot reactRoot) {
861864
UiThreadUtil.assertOnUiThread();
@@ -865,35 +868,37 @@ public void attachRootView(ReactRoot reactRoot) {
865868
// Ideally reactRoot should be initialized with id == NO_ID
866869
if (mAttachedReactRoots.add(reactRoot)) {
867870
clearReactRoot(reactRoot);
871+
} else {
872+
FLog.e(ReactConstants.TAG, "ReactRoot was attached multiple times");
868873
}
869874

870875
// If react context is being created in the background, JS application will be started
871876
// automatically when creation completes, as reactRoot is part of the attached
872877
// reactRoot list.
873878
ReactContext currentContext = getCurrentReactContext();
874879
if (mCreateReactContextThread == null && currentContext != null) {
875-
if (reactRoot.getState().compareAndSet(ReactRoot.STATE_STOPPED, ReactRoot.STATE_STARTED)) {
876-
attachRootViewToInstance(reactRoot);
877-
}
880+
attachRootViewToInstance(reactRoot);
878881
}
879882
}
880883

881884
/**
882885
* Detach given {@param reactRoot} from current catalyst instance. It's safe to call this method
883886
* multiple times on the same {@param reactRoot} - in that case view will be detached with the
884887
* first call.
888+
*
889+
* @deprecated This method should be internal to ReactRootView and ReactInstanceManager
885890
*/
891+
@Deprecated
886892
@ThreadConfined(UI)
887893
public void detachRootView(ReactRoot reactRoot) {
888894
UiThreadUtil.assertOnUiThread();
889-
synchronized (mAttachedReactRoots) {
890-
if (mAttachedReactRoots.contains(reactRoot)) {
891-
ReactContext currentContext = getCurrentReactContext();
892-
mAttachedReactRoots.remove(reactRoot);
893-
if (currentContext != null && currentContext.hasActiveReactInstance()) {
894-
detachViewFromInstance(reactRoot, currentContext.getCatalystInstance());
895-
}
896-
}
895+
if (!mAttachedReactRoots.remove(reactRoot)) {
896+
return;
897+
}
898+
899+
ReactContext reactContext = mCurrentReactContext;
900+
if (reactContext != null && reactContext.hasActiveReactInstance()) {
901+
detachRootViewFromInstance(reactRoot, reactContext);
897902
}
898903
}
899904

@@ -1150,9 +1155,7 @@ private void setupReactContext(final ReactApplicationContext reactContext) {
11501155

11511156
ReactMarker.logMarker(ATTACH_MEASURED_ROOT_VIEWS_START);
11521157
for (ReactRoot reactRoot : mAttachedReactRoots) {
1153-
if (reactRoot.getState().compareAndSet(ReactRoot.STATE_STOPPED, ReactRoot.STATE_STARTED)) {
1154-
attachRootViewToInstance(reactRoot);
1155-
}
1158+
attachRootViewToInstance(reactRoot);
11561159
}
11571160
ReactMarker.logMarker(ATTACH_MEASURED_ROOT_VIEWS_END);
11581161
}
@@ -1194,6 +1197,11 @@ private void setupReactContext(final ReactApplicationContext reactContext) {
11941197

11951198
private void attachRootViewToInstance(final ReactRoot reactRoot) {
11961199
FLog.d(ReactConstants.TAG, "ReactInstanceManager.attachRootViewToInstance()");
1200+
if (!reactRoot.getState().compareAndSet(ReactRoot.STATE_STOPPED, ReactRoot.STATE_STARTED)) {
1201+
// Already started
1202+
return;
1203+
}
1204+
11971205
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "attachRootViewToInstance");
11981206

11991207
@Nullable
@@ -1210,7 +1218,6 @@ private void attachRootViewToInstance(final ReactRoot reactRoot) {
12101218
@Nullable Bundle initialProperties = reactRoot.getAppProperties();
12111219

12121220
final int rootTag;
1213-
12141221
if (reactRoot.getUIManagerType() == FABRIC) {
12151222
rootTag =
12161223
uiManager.startSurface(
@@ -1245,18 +1252,48 @@ private void attachRootViewToInstance(final ReactRoot reactRoot) {
12451252
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
12461253
}
12471254

1248-
private void detachViewFromInstance(ReactRoot reactRoot, CatalystInstance catalystInstance) {
1249-
FLog.d(ReactConstants.TAG, "ReactInstanceManager.detachViewFromInstance()");
1255+
private void detachRootViewFromInstance(ReactRoot reactRoot, ReactContext reactContext) {
1256+
FLog.d(ReactConstants.TAG, "ReactInstanceManager.detachRootViewFromInstance()");
12501257
UiThreadUtil.assertOnUiThread();
1251-
if (reactRoot.getUIManagerType() == FABRIC) {
1252-
catalystInstance
1253-
.getJSModule(ReactFabric.class)
1254-
.unmountComponentAtNode(reactRoot.getRootViewTag());
1258+
1259+
if (!reactRoot.getState().compareAndSet(ReactRoot.STATE_STARTED, ReactRoot.STATE_STOPPED)) {
1260+
// ReactRoot was already stopped
1261+
return;
1262+
}
1263+
1264+
@UIManagerType int uiManagerType = reactRoot.getUIManagerType();
1265+
if (uiManagerType == UIManagerType.FABRIC) {
1266+
// Stop surface in Fabric.
1267+
// Calling FabricUIManager.stopSurface causes the C++ Binding.stopSurface
1268+
// to be called synchronously over the JNI, which causes an empty tree
1269+
// to be committed via the Scheduler, which will cause mounting instructions
1270+
// to be queued up and synchronously executed to delete and remove
1271+
// all the views in the hierarchy.
1272+
final int surfaceId = reactRoot.getRootViewTag();
1273+
if (surfaceId != View.NO_ID) {
1274+
UIManager uiManager = UIManagerHelper.getUIManager(reactContext, uiManagerType);
1275+
if (uiManager != null) {
1276+
uiManager.stopSurface(surfaceId);
1277+
} else {
1278+
FLog.w(ReactConstants.TAG, "Failed to stop surface, UIManager has already gone away");
1279+
reactRoot.getRootViewGroup().removeAllViews();
1280+
}
1281+
} else {
1282+
ReactSoftExceptionLogger.logSoftException(
1283+
TAG,
1284+
new RuntimeException(
1285+
"detachRootViewFromInstance called with ReactRootView with invalid id"));
1286+
reactRoot.getRootViewGroup().removeAllViews();
1287+
}
12551288
} else {
1256-
catalystInstance
1289+
reactContext
1290+
.getCatalystInstance()
12571291
.getJSModule(AppRegistry.class)
12581292
.unmountApplicationComponentAtRootTag(reactRoot.getRootViewTag());
12591293
}
1294+
1295+
// The view is no longer attached, so mark it as such by resetting its ID.
1296+
reactRoot.getRootViewGroup().setId(View.NO_ID);
12601297
}
12611298

12621299
@ThreadConfined(UI)
@@ -1269,7 +1306,7 @@ private void tearDownReactContext(ReactContext reactContext) {
12691306

12701307
synchronized (mAttachedReactRoots) {
12711308
for (ReactRoot reactRoot : mAttachedReactRoots) {
1272-
clearReactRoot(reactRoot);
1309+
detachRootViewFromInstance(reactRoot, reactContext);
12731310
}
12741311
}
12751312

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -611,41 +611,6 @@ private void updateRootLayoutSpecs(
611611
@ThreadConfined(UI)
612612
public void unmountReactApplication() {
613613
UiThreadUtil.assertOnUiThread();
614-
// Stop surface in Fabric.
615-
// Calling FabricUIManager.stopSurface causes the C++ Binding.stopSurface
616-
// to be called synchronously over the JNI, which causes an empty tree
617-
// to be committed via the Scheduler, which will cause mounting instructions
618-
// to be queued up and synchronously executed to delete and remove
619-
// all the views in the hierarchy.
620-
if (hasActiveReactInstance()) {
621-
final ReactContext reactApplicationContext = getCurrentReactContext();
622-
if (reactApplicationContext != null && isFabric()) {
623-
@Nullable
624-
UIManager uiManager =
625-
UIManagerHelper.getUIManager(reactApplicationContext, getUIManagerType());
626-
if (uiManager != null) {
627-
final int surfaceId = this.getId();
628-
629-
// In case of "retry" or error dialogues being shown, this ReactRootView could be
630-
// reused (with the same surfaceId, or a different one). Ensure the ReactRootView
631-
// is marked as unused in case of that.
632-
setId(NO_ID);
633-
634-
// Remove all children from ReactRootView
635-
removeAllViews();
636-
637-
if (surfaceId == NO_ID) {
638-
ReactSoftExceptionLogger.logSoftException(
639-
TAG,
640-
new RuntimeException(
641-
"unmountReactApplication called on ReactRootView with invalid id"));
642-
} else {
643-
uiManager.stopSurface(surfaceId);
644-
}
645-
}
646-
}
647-
}
648-
649614
if (mReactInstanceManager != null && mIsAttachedToInstance) {
650615
mReactInstanceManager.detachRootView(this);
651616
mIsAttachedToInstance = false;

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fabric/ReactFabric.java

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)