@@ -3603,9 +3603,14 @@ export function attach(
36033603 } else {
36043604 const contentFiber = fiber.child;
36053605 if (contentFiber === null) {
3606- throw new Error(
3607- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3608- );
3606+ const suspenseState = fiber.memoizedState;
3607+ if (suspenseState === null || suspenseState.dehydrated === null) {
3608+ throw new Error(
3609+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3610+ );
3611+ }
3612+ // This Suspense Fiber is still dehydrated. It won't have any children
3613+ // until hydration.
36093614 }
36103615 const isTimedOut = fiber.memoizedState !== null;
36113616 if (!isTimedOut) {
@@ -3654,12 +3659,17 @@ export function attach(
36543659 }
36553660 } else {
36563661 const contentFiber = fiber.child;
3662+ const suspenseState = fiber.memoizedState;
36573663 if (contentFiber === null) {
3658- throw new Error(
3659- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3660- );
3664+ if (suspenseState === null || suspenseState.dehydrated === null) {
3665+ throw new Error(
3666+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3667+ );
3668+ }
3669+ // This Suspense Fiber is still dehydrated. It won't have any children
3670+ // until hydration.
36613671 }
3662- const isTimedOut = fiber.memoizedState !== null;
3672+ const isTimedOut = suspenseState !== null;
36633673 if (!isTimedOut) {
36643674 newSuspenseNode.rects = measureInstance(newInstance);
36653675 }
@@ -3787,34 +3797,40 @@ export function attach(
37873797 // Modern Suspense path
37883798 const contentFiber = fiber.child;
37893799 if (contentFiber === null) {
3790- throw new Error(
3791- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3792- );
3793- }
3794- const fallbackFiber = contentFiber.sibling;
3795-
3796- // First update only the Offscreen boundary. I.e. the main content.
3797- mountVirtualChildrenRecursively(
3798- contentFiber,
3799- fallbackFiber,
3800- traceNearestHostComponentUpdate,
3801- 0, // first level
3802- );
3800+ const suspenseState = fiber.memoizedState;
3801+ if (suspenseState === null || suspenseState.dehydrated === null) {
3802+ throw new Error(
3803+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3804+ );
3805+ }
3806+ // This Suspense Fiber is still dehydrated. It won't have any children
3807+ // until hydration.
3808+ } else {
3809+ const fallbackFiber = contentFiber.sibling;
38033810
3804- // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3805- // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3806- // Since the fallback conceptually blocks the parent.
3807- reconcilingParentSuspenseNode = stashedSuspenseParent;
3808- previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3809- remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3810- shouldPopSuspenseNode = false;
3811- if (fallbackFiber !== null) {
3811+ // First update only the Offscreen boundary. I.e. the main content.
38123812 mountVirtualChildrenRecursively(
3813+ contentFiber,
38133814 fallbackFiber,
3814- null,
38153815 traceNearestHostComponentUpdate,
38163816 0, // first level
38173817 );
3818+
3819+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3820+ // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3821+ // Since the fallback conceptually blocks the parent.
3822+ reconcilingParentSuspenseNode = stashedSuspenseParent;
3823+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3824+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3825+ shouldPopSuspenseNode = false;
3826+ if (fallbackFiber !== null) {
3827+ mountVirtualChildrenRecursively(
3828+ fallbackFiber,
3829+ null,
3830+ traceNearestHostComponentUpdate,
3831+ 0, // first level
3832+ );
3833+ }
38183834 }
38193835 } else {
38203836 if (fiber.child !== null) {
@@ -4719,61 +4735,74 @@ export function attach(
47194735 const prevContentFiber = prevFiber.child;
47204736 const nextContentFiber = nextFiber.child;
47214737 if (nextContentFiber === null || prevContentFiber === null) {
4722- throw new Error(
4723- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
4738+ const previousSuspenseState = prevFiber.memoizedState;
4739+ const nextSuspenseState = nextFiber.memoizedState;
4740+ if (
4741+ previousSuspenseState === null ||
4742+ previousSuspenseState.dehydrated === null ||
4743+ nextSuspenseState === null ||
4744+ nextSuspenseState.dehydrated === null
4745+ ) {
4746+ throw new Error(
4747+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4748+ );
4749+ }
4750+ // This Suspense Fiber is still dehydrated. It won't have any children
4751+ // until hydration.
4752+ } else {
4753+ const prevFallbackFiber = prevContentFiber.sibling;
4754+ const nextFallbackFiber = nextContentFiber.sibling;
4755+
4756+ // First update only the Offscreen boundary. I.e. the main content.
4757+ updateFlags |= updateVirtualChildrenRecursively(
4758+ nextContentFiber,
4759+ nextFallbackFiber,
4760+ prevContentFiber,
4761+ traceNearestHostComponentUpdate,
4762+ 0,
47244763 );
4725- }
4726- const prevFallbackFiber = prevContentFiber.sibling;
4727- const nextFallbackFiber = nextContentFiber.sibling;
4728-
4729- // First update only the Offscreen boundary. I.e. the main content.
4730- updateFlags |= updateVirtualChildrenRecursively(
4731- nextContentFiber,
4732- nextFallbackFiber,
4733- prevContentFiber,
4734- traceNearestHostComponentUpdate,
4735- 0,
4736- );
47374764
4738- shouldMeasureSuspenseNode = false;
4739- if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4740- const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4741- const fallbackStashedSuspensePrevious =
4742- previouslyReconciledSiblingSuspenseNode;
4743- const fallbackStashedSuspenseRemaining =
4744- remainingReconcilingChildrenSuspenseNodes;
4745- // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4746- // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4747- // Since the fallback conceptually blocks the parent.
4748- reconcilingParentSuspenseNode = stashedSuspenseParent;
4749- previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4750- remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4751- try {
4752- if (nextFallbackFiber === null) {
4753- unmountRemainingChildren();
4754- } else {
4755- updateFlags |= updateVirtualChildrenRecursively(
4756- nextFallbackFiber,
4757- null,
4758- prevFallbackFiber,
4759- traceNearestHostComponentUpdate,
4760- 0,
4761- );
4762- }
4763- } finally {
4764- reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4765- previouslyReconciledSiblingSuspenseNode =
4766- fallbackStashedSuspensePrevious;
4765+ shouldMeasureSuspenseNode = false;
4766+ if (prevFallbackFiber || nextFallbackFiber !== null) {
4767+ const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4768+ const fallbackStashedSuspensePrevious =
4769+ previouslyReconciledSiblingSuspenseNode;
4770+ const fallbackStashedSuspenseRemaining =
4771+ remainingReconcilingChildrenSuspenseNodes;
4772+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4773+ // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4774+ // Since the fallback conceptually blocks the parent.
4775+ reconcilingParentSuspenseNode = stashedSuspenseParent;
4776+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
47674777 remainingReconcilingChildrenSuspenseNodes =
4768- fallbackStashedSuspenseRemaining;
4778+ stashedSuspenseRemaining;
4779+ try {
4780+ if (nextFallbackFiber === null) {
4781+ unmountRemainingChildren();
4782+ } else {
4783+ updateFlags |= updateVirtualChildrenRecursively(
4784+ nextFallbackFiber,
4785+ null,
4786+ prevFallbackFiber,
4787+ traceNearestHostComponentUpdate,
4788+ 0,
4789+ );
4790+ }
4791+ } finally {
4792+ reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4793+ previouslyReconciledSiblingSuspenseNode =
4794+ fallbackStashedSuspensePrevious;
4795+ remainingReconcilingChildrenSuspenseNodes =
4796+ fallbackStashedSuspenseRemaining;
4797+ }
4798+ }
4799+ if (nextFiber.memoizedState === null) {
4800+ // Measure this Suspense node in case it changed. We don't update the rect while
4801+ // we're inside a disconnected subtree nor if we are the Suspense boundary that
4802+ // is suspended. This lets us keep the rectangle of the displayed content while
4803+ // we're suspended to visualize the resulting state.
4804+ shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
47694805 }
4770- }
4771- if (nextFiber.memoizedState === null) {
4772- // Measure this Suspense node in case it changed. We don't update the rect while
4773- // we're inside a disconnected subtree nor if we are the Suspense boundary that
4774- // is suspended. This lets us keep the rectangle of the displayed content while
4775- // we're suspended to visualize the resulting state.
4776- shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
47774806 }
47784807 } else {
47794808 // Common case: Primary -> Primary.
0 commit comments