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) {