diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/React.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/React.java
index 83db27ded7..7ab512c713 100644
--- a/src/main/java/org/sagebionetworks/web/client/jsinterop/React.java
+++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/React.java
@@ -10,10 +10,7 @@ public class React {
public static native <
T extends ReactComponentType
, P extends ReactComponentProps
- > ReactElement createElement(
- ReactComponentType componentType,
- P props
- );
+ > ReactElement createElement(ReactComponentType componentType);
public static native <
T extends ReactComponentType
, P extends ReactComponentProps
@@ -83,4 +80,6 @@ public static native ReactElement cloneElement(
ReactComponentProps props,
ReactElement... children
);
+
+ public static ReactComponentType Fragment;
}
diff --git a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java
index ff28cc3509..ce8839c311 100644
--- a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java
+++ b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java
@@ -7,6 +7,7 @@
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.FlowPanel;
+import org.sagebionetworks.web.client.jsinterop.React;
import org.sagebionetworks.web.client.jsinterop.ReactDOM;
import org.sagebionetworks.web.client.jsinterop.ReactDOMRoot;
import org.sagebionetworks.web.client.jsinterop.ReactElement;
@@ -33,12 +34,30 @@ private void createRoot() {
}
}
- public void render(ReactElement, ?> reactElement) {
- this.reactElement = reactElement;
+ /**
+ * Asynchronously (in the task queue, via setTimeout) unmounts the root and sets it to null.
+ */
+ private void destroyRoot() {
+ // React itself may have fired this method in its render cycle. If that's the case, we cannot unmount synchronously.
+ // We can asynchronously schedule unmounting the root to allow React to finish the current render cycle.
+ // https://github.com/facebook/react/issues/25675
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ if (root != null) {
+ root.unmount();
+ root = null;
+ }
+ }
+ };
+ t.schedule(0);
+ }
- // This component may be a React child of another component. If so, we must rerender the ancestor component(s) so
- // that they use the new ReactElement created in this render step.
- // Asynchronously schedule creating a root in case React is still rendering and may unmount the current root
+ /**
+ * Asynchronously (in the task queue, via setTimeout) creates a root (if necessary) and renders the current reactElement.
+ */
+ private void createRootAndRender() {
+ // Asynchronously schedule createRoot and render to ensure any prequeued `destroyRoot` task completes first
Timer t = new Timer() {
@Override
public void run() {
@@ -50,10 +69,16 @@ public void run() {
t.schedule(0);
}
+ public void render(ReactElement, ?> reactElement) {
+ this.reactElement = reactElement;
+ createRootAndRender();
+ }
+
@Override
protected void onLoad() {
super.onLoad();
createRoot();
+
if (reactElement != null) {
this.render(reactElement);
}
@@ -61,26 +86,15 @@ protected void onLoad() {
@Override
protected void onUnload() {
- if (root != null) {
- // Asynchronously schedule unmounting the root to allow React to finish the current render cycle.
- // https://github.com/facebook/react/issues/25675
- Timer t = new Timer() {
- @Override
- public void run() {
- root.unmount();
- root = null;
- }
- };
- t.schedule(0);
- }
+ destroyRoot();
super.onUnload();
}
@Override
public void clear() {
- // clear doesn't typically call onUnload, but we want to for this element.
- this.onUnload();
- super.clear();
+ if (root != null) {
+ root.render(React.createElement(React.Fragment));
+ }
}
@Override
diff --git a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java
index 1877114ba9..14912a1460 100644
--- a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java
+++ b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java
@@ -92,11 +92,40 @@ private void createRoot() {
}
}
+ /**
+ * Asynchronously (in the task queue, via setTimeout) unmounts the root and sets it to null.
+ */
private void destroyRoot() {
- if (root != null) {
- root.unmount();
- root = null;
- }
+ // React itself may have fired this method in its render cycle. If that's the case, we cannot unmount synchronously.
+ // We can asynchronously schedule unmounting the root to allow React to finish the current render cycle.
+ // https://github.com/facebook/react/issues/25675
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ if (root != null) {
+ root.unmount();
+ root = null;
+ }
+ }
+ };
+ t.schedule(0);
+ }
+
+ /**
+ * Asynchronously (in the task queue, via setTimeout) creates a root (if necessary) and renders the current reactElement.
+ */
+ private void createRootAndRender() {
+ // This component may be a React child of another component, so retrieve the root widget that renders this component tree.
+ ReactComponentV2, ?> componentToRender = getRootReactComponentWidget();
+ // Schedule creating a root and rendering. This will necessarily run after the `destroyRoot` completes, if it was invoked.
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ componentToRender.createRoot();
+ componentToRender.root.render(componentToRender.createReactElement());
+ }
+ };
+ t.schedule(0);
}
private void detachNonReactChildElements() {
@@ -181,24 +210,13 @@ public void render() {
// This component will be rendered as a child of another React component, so destroy the root if one exists
boolean shouldDestroyRoot = isRenderedAsReactComponentChild();
- // This component may be a React child of another component, so retrieve the root widget that renders this component tree.
- ReactComponentV2, ?> componentToRender = getRootReactComponentWidget();
-
- // Asynchronously schedule root operations in case the component is in the middle of an asynchronous render cycle
- // See https://stackoverflow.com/questions/73459382
- Timer t = new Timer() {
- @Override
- public void run() {
- if (shouldDestroyRoot) {
- destroyRoot();
- }
+ // Schedule destroying the root, if necessary
+ if (shouldDestroyRoot) {
+ destroyRoot();
+ }
- // Create a fresh ReactElement tree and render it
- componentToRender.createRoot();
- componentToRender.root.render(componentToRender.createReactElement());
- }
- };
- t.schedule(0);
+ // Schedule rendering
+ createRootAndRender();
}
@Override
@@ -216,20 +234,12 @@ protected void onLoad() {
@Override
protected void onUnload() {
- super.onUnload();
-
// Detach any non-React descendants that were injected into the component tree
detachNonReactChildElements();
- // Asynchronously schedule unmounting the root to allow React to finish the current render cycle.
- // https://github.com/facebook/react/issues/25675
- Timer t = new Timer() {
- @Override
- public void run() {
- destroyRoot();
- }
- };
- t.schedule(0);
+ destroyRoot();
+
+ super.onUnload();
}
/**