diff --git a/shells/dev/app/SuspenseTree/index.js b/shells/dev/app/SuspenseTree/index.js
new file mode 100644
index 0000000000..fb4ca80de6
--- /dev/null
+++ b/shells/dev/app/SuspenseTree/index.js
@@ -0,0 +1,64 @@
+// @flow
+
+import React, { Suspense, useState } from 'react';
+
+function SuspenseTree() {
+ return (
+ <>
+
Suspense
+ Loading outer}>
+
+
+ >
+ );
+}
+
+function Parent() {
+ return (
+
+ Loading inner 1}>
+ Hello
+
+ Loading inner 2}>
+ World
+
+ This will never load}>
+
+
+
+
+ );
+}
+
+function LoadLater() {
+ const [loadChild, setLoadChild] = useState(0);
+ return (
+ setLoadChild(true)}>Click to load
+ }
+ >
+ {loadChild ? (
+ setLoadChild(false)}>
+ Loaded! Click to suspend again.
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function Child(props) {
+ return ;
+}
+
+function Fallback(props) {
+ return {props.children}
;
+}
+
+function Never() {
+ throw new Promise(resolve => {});
+}
+
+export default SuspenseTree;
diff --git a/shells/dev/app/index.js b/shells/dev/app/index.js
index 4857c0c2cf..3a76db783f 100644
--- a/shells/dev/app/index.js
+++ b/shells/dev/app/index.js
@@ -11,6 +11,7 @@ import InspectableElements from './InspectableElements';
import InteractionTracing from './InteractionTracing';
import ToDoList from './ToDoList';
import Toggle from './Toggle';
+import SuspenseTree from './SuspenseTree';
import './styles.css';
@@ -33,6 +34,7 @@ function mountTestApp() {
mountHelper(ElementTypes);
mountHelper(EditableProps);
mountHelper(Toggle);
+ mountHelper(SuspenseTree);
mountHelper(DeeplyNestedComponents);
}
diff --git a/src/backend/renderer.js b/src/backend/renderer.js
index cf0b973e68..099e0b3743 100644
--- a/src/backend/renderer.js
+++ b/src/backend/renderer.js
@@ -716,6 +716,16 @@ export function attach(
function enqueueUnmount(fiber) {
const isRoot = fiber.tag === HostRoot;
const primaryFiber = getPrimaryFiber(fiber);
+ if (!fiberToIDMap.has(primaryFiber)) {
+ // If we've never seen this Fiber, it might be because
+ // it is inside a non-current Suspense fragment tree,
+ // and so the store is not even aware of it.
+ // In that case we can just ignore it, or otherwise
+ // there will be errors later on.
+ primaryFibers.delete(primaryFiber);
+ // TODO: this is fragile and can obscure actual bugs.
+ return;
+ }
const id = getFiberID(primaryFiber);
if (isRoot) {
const operation = new Uint32Array(2);
@@ -757,8 +767,24 @@ export function attach(
enqueueMount(fiber, parentFiber);
}
- if (fiber.child !== null) {
- mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber, true);
+ const isTimedOutSuspense =
+ fiber.tag === ReactTypeOfWork.SuspenseComponent &&
+ fiber.memoizedState !== null;
+
+ if (isTimedOutSuspense) {
+ // Special case: if Suspense mounts in a timed-out state,
+ // get the fallback child from the inner fragment and mount
+ // it as if it was our own child. Updates handle this too.
+ const primaryChildFragment = fiber.child;
+ const fallbackChildFragment = primaryChildFragment.sibling;
+ const fallbackChild = fallbackChildFragment.child;
+ if (fallbackChild !== null) {
+ mountFiber(fallbackChild, shouldEnqueueMount ? fiber : parentFiber, true);
+ }
+ } else {
+ if (fiber.child !== null) {
+ mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber, true);
+ }
}
if (traverseSiblings && fiber.sibling !== null) {