diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 94c394cbd701b..fc054d817d46b 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1543,6 +1543,22 @@ export function attach( return Array.from(knownEnvironmentNames); } + function isFiberHydrated(fiber: Fiber): boolean { + if (OffscreenComponent === -1) { + throw new Error('not implemented for legacy suspense'); + } + switch (fiber.tag) { + case HostRoot: + const rootState = fiber.memoizedState; + return !rootState.isDehydrated; + case SuspenseComponent: + const suspenseState = fiber.memoizedState; + return suspenseState === null || suspenseState.dehydrated === null; + default: + throw new Error('not implemented for work tag ' + fiber.tag); + } + } + function shouldFilterVirtual( data: ReactComponentInfo, secondaryEnv: null | string, @@ -3610,6 +3626,50 @@ export function attach( ); } + function mountSuspenseChildrenRecursively( + contentFiber: Fiber, + traceNearestHostComponentUpdate: boolean, + stashedSuspenseParent: SuspenseNode | null, + stashedSuspensePrevious: SuspenseNode | null, + stashedSuspenseRemaining: SuspenseNode | null, + ) { + const fallbackFiber = contentFiber.sibling; + + // First update only the Offscreen boundary. I.e. the main content. + mountVirtualChildrenRecursively( + contentFiber, + fallbackFiber, + traceNearestHostComponentUpdate, + 0, // first level + ); + + if (fallbackFiber !== null) { + const fallbackStashedSuspenseParent = stashedSuspenseParent; + const fallbackStashedSuspensePrevious = stashedSuspensePrevious; + const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining; + // Next, we'll pop back out of the SuspenseNode that we added above and now we'll + // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode. + // Since the fallback conceptually blocks the parent. + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + try { + mountVirtualChildrenRecursively( + fallbackFiber, + null, + traceNearestHostComponentUpdate, + 0, // first level + ); + } finally { + reconcilingParentSuspenseNode = fallbackStashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = + fallbackStashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = + fallbackStashedSuspenseRemaining; + } + } + } + function mountFiberRecursively( fiber: Fiber, traceNearestHostComponentUpdate: boolean, @@ -3632,11 +3692,17 @@ export function attach( newSuspenseNode.rects = measureInstance(newInstance); } } else { - const contentFiber = fiber.child; - if (contentFiber === null) { - throw new Error( - 'There should always be an Offscreen Fiber child in a Suspense boundary.', - ); + const hydrated = isFiberHydrated(fiber); + if (hydrated) { + const contentFiber = fiber.child; + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. } const isTimedOut = fiber.memoizedState !== null; if (!isTimedOut) { @@ -3684,13 +3750,20 @@ export function attach( newSuspenseNode.rects = measureInstance(newInstance); } } else { - const contentFiber = fiber.child; - if (contentFiber === null) { - throw new Error( - 'There should always be an Offscreen Fiber child in a Suspense boundary.', - ); + const hydrated = isFiberHydrated(fiber); + if (hydrated) { + const contentFiber = fiber.child; + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. } - const isTimedOut = fiber.memoizedState !== null; + const suspenseState = fiber.memoizedState; + const isTimedOut = suspenseState !== null; if (!isTimedOut) { newSuspenseNode.rects = measureInstance(newInstance); } @@ -3820,38 +3893,26 @@ export function attach( ) { // Modern Suspense path const contentFiber = fiber.child; - if (contentFiber === null) { - throw new Error( - 'There should always be an Offscreen Fiber child in a Suspense boundary.', - ); - } - - trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode); - - const fallbackFiber = contentFiber.sibling; + const hydrated = isFiberHydrated(fiber); + if (hydrated) { + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } - // First update only the Offscreen boundary. I.e. the main content. - mountVirtualChildrenRecursively( - contentFiber, - fallbackFiber, - traceNearestHostComponentUpdate, - 0, // first level - ); + trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode); - // Next, we'll pop back out of the SuspenseNode that we added above and now we'll - // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode. - // Since the fallback conceptually blocks the parent. - reconcilingParentSuspenseNode = stashedSuspenseParent; - previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; - remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; - shouldPopSuspenseNode = false; - if (fallbackFiber !== null) { - mountVirtualChildrenRecursively( - fallbackFiber, - null, + mountSuspenseChildrenRecursively( + contentFiber, traceNearestHostComponentUpdate, - 0, // first level + stashedSuspenseParent, + stashedSuspensePrevious, + stashedSuspenseRemaining, ); + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. } } else { if (fiber.child !== null) { @@ -4505,6 +4566,63 @@ export function attach( ); } + function updateSuspenseChildrenRecursively( + nextContentFiber: Fiber, + prevContentFiber: Fiber, + traceNearestHostComponentUpdate: boolean, + stashedSuspenseParent: null | SuspenseNode, + stashedSuspensePrevious: null | SuspenseNode, + stashedSuspenseRemaining: null | SuspenseNode, + ): number { + let updateFlags = NoUpdate; + const prevFallbackFiber = prevContentFiber.sibling; + const nextFallbackFiber = nextContentFiber.sibling; + + // First update only the Offscreen boundary. I.e. the main content. + updateFlags |= updateVirtualChildrenRecursively( + nextContentFiber, + nextFallbackFiber, + prevContentFiber, + traceNearestHostComponentUpdate, + 0, + ); + + if (prevFallbackFiber !== null || nextFallbackFiber !== null) { + const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode; + const fallbackStashedSuspensePrevious = + previouslyReconciledSiblingSuspenseNode; + const fallbackStashedSuspenseRemaining = + remainingReconcilingChildrenSuspenseNodes; + // Next, we'll pop back out of the SuspenseNode that we added above and now we'll + // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode. + // Since the fallback conceptually blocks the parent. + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + try { + if (nextFallbackFiber === null) { + unmountRemainingChildren(); + } else { + updateFlags |= updateVirtualChildrenRecursively( + nextFallbackFiber, + null, + prevFallbackFiber, + traceNearestHostComponentUpdate, + 0, + ); + } + } finally { + reconcilingParentSuspenseNode = fallbackStashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = + fallbackStashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = + fallbackStashedSuspenseRemaining; + } + } + + return updateFlags; + } + // Returns whether closest unfiltered fiber parent needs to reset its child list. function updateFiberRecursively( fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered @@ -4765,71 +4883,67 @@ export function attach( fiberInstance.suspenseNode !== null ) { // Modern Suspense path + const suspenseNode = fiberInstance.suspenseNode; const prevContentFiber = prevFiber.child; const nextContentFiber = nextFiber.child; - if (nextContentFiber === null || prevContentFiber === null) { - throw new Error( - 'There should always be an Offscreen Fiber child in a Suspense boundary.', - ); - } - const prevFallbackFiber = prevContentFiber.sibling; - const nextFallbackFiber = nextContentFiber.sibling; + const previousHydrated = isFiberHydrated(prevFiber); + const nextHydrated = isFiberHydrated(nextFiber); + if (previousHydrated && nextHydrated) { + if (nextContentFiber === null || prevContentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } - if ((prevFiber.stateNode === null) !== (nextFiber.stateNode === null)) { - trackThrownPromisesFromRetryCache( - fiberInstance.suspenseNode, - nextFiber.stateNode, + if ( + (prevFiber.stateNode === null) !== + (nextFiber.stateNode === null) + ) { + trackThrownPromisesFromRetryCache( + suspenseNode, + nextFiber.stateNode, + ); + } + + shouldMeasureSuspenseNode = false; + updateFlags |= updateSuspenseChildrenRecursively( + nextContentFiber, + prevContentFiber, + traceNearestHostComponentUpdate, + stashedSuspenseParent, + stashedSuspensePrevious, + stashedSuspenseRemaining, ); - } + if (nextFiber.memoizedState === null) { + // Measure this Suspense node in case it changed. We don't update the rect while + // we're inside a disconnected subtree nor if we are the Suspense boundary that + // is suspended. This lets us keep the rectangle of the displayed content while + // we're suspended to visualize the resulting state. + shouldMeasureSuspenseNode = !isInDisconnectedSubtree; + } + } else if (!previousHydrated && nextHydrated) { + if (nextContentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } - // First update only the Offscreen boundary. I.e. the main content. - updateFlags |= updateVirtualChildrenRecursively( - nextContentFiber, - nextFallbackFiber, - prevContentFiber, - traceNearestHostComponentUpdate, - 0, - ); + trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode); - shouldMeasureSuspenseNode = false; - if (prevFallbackFiber !== null || nextFallbackFiber !== null) { - const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode; - const fallbackStashedSuspensePrevious = - previouslyReconciledSiblingSuspenseNode; - const fallbackStashedSuspenseRemaining = - remainingReconcilingChildrenSuspenseNodes; - // Next, we'll pop back out of the SuspenseNode that we added above and now we'll - // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode. - // Since the fallback conceptually blocks the parent. - reconcilingParentSuspenseNode = stashedSuspenseParent; - previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; - remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; - try { - if (nextFallbackFiber === null) { - unmountRemainingChildren(); - } else { - updateFlags |= updateVirtualChildrenRecursively( - nextFallbackFiber, - null, - prevFallbackFiber, - traceNearestHostComponentUpdate, - 0, - ); - } - } finally { - reconcilingParentSuspenseNode = fallbackStashedSuspenseParent; - previouslyReconciledSiblingSuspenseNode = - fallbackStashedSuspensePrevious; - remainingReconcilingChildrenSuspenseNodes = - fallbackStashedSuspenseRemaining; - } - } - if (nextFiber.memoizedState === null) { - // Measure this Suspense node in case it changed. We don't update the rect while - // we're inside a disconnected subtree nor if we are the Suspense boundary that - // is suspended. This lets us keep the rectangle of the displayed content while - // we're suspended to visualize the resulting state. - shouldMeasureSuspenseNode = !isInDisconnectedSubtree; + mountSuspenseChildrenRecursively( + nextContentFiber, + traceNearestHostComponentUpdate, + stashedSuspenseParent, + stashedSuspensePrevious, + stashedSuspenseRemaining, + ); + } else if (previousHydrated && !nextHydrated) { + throw new Error( + 'Encountered a dehydrated Suspense boundary that was previously hydrated.', + ); + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. } } else { // Common case: Primary -> Primary. @@ -5164,14 +5278,9 @@ export function attach( // TODO: relying on this seems a bit fishy. const wasMounted = prevFiber.memoizedState != null && - prevFiber.memoizedState.element != null && - // A dehydrated root is not considered mounted - prevFiber.memoizedState.isDehydrated !== true; + prevFiber.memoizedState.element != null; const isMounted = - current.memoizedState != null && - current.memoizedState.element != null && - // A dehydrated root is not considered mounted - current.memoizedState.isDehydrated !== true; + current.memoizedState != null && current.memoizedState.element != null; if (!wasMounted && isMounted) { // Mount a new root. setRootPseudoKey(currentRoot.id, current); diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index ea921c2988c3d..34c258ebe2b98 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -300,7 +300,7 @@ export function printOperationsArray(operations: Array) { } case TREE_OPERATION_SET_SUBTREE_MODE: { const id = operations[i + 1]; - const mode = operations[i + 1]; + const mode = operations[i + 2]; i += 3; @@ -339,11 +339,11 @@ export function printOperationsArray(operations: Array) { const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const name = stringTable[nameStringID]; const numRects = operations[i + 4]; i += 5; + const name = stringTable[nameStringID]; let rects: string; if (numRects === -1) { rects = 'null'; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 372a74f97b205..52b9b9ebe582c 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1796,7 +1796,7 @@ function updateHostRoot( } const nextProps = workInProgress.pendingProps; - const prevState = workInProgress.memoizedState; + const prevState: RootState = workInProgress.memoizedState; const prevChildren = prevState.element; cloneUpdateQueue(current, workInProgress); processUpdateQueue(workInProgress, nextProps, null, renderLanes);