@@ -3634,9 +3634,14 @@ export function attach(
36343634 } else {
36353635 const contentFiber = fiber.child;
36363636 if (contentFiber === null) {
3637- throw new Error(
3638- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3639- );
3637+ const suspenseState = fiber.memoizedState;
3638+ if (suspenseState === null || suspenseState.dehydrated === null) {
3639+ throw new Error(
3640+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3641+ );
3642+ }
3643+ // This Suspense Fiber is still dehydrated. It won't have any children
3644+ // until hydration.
36403645 }
36413646 const isTimedOut = fiber.memoizedState !== null;
36423647 if (!isTimedOut) {
@@ -3685,12 +3690,17 @@ export function attach(
36853690 }
36863691 } else {
36873692 const contentFiber = fiber.child;
3693+ const suspenseState = fiber.memoizedState;
36883694 if (contentFiber === null) {
3689- throw new Error(
3690- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3691- );
3695+ if (suspenseState === null || suspenseState.dehydrated === null) {
3696+ throw new Error(
3697+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3698+ );
3699+ }
3700+ // This Suspense Fiber is still dehydrated. It won't have any children
3701+ // until hydration.
36923702 }
3693- const isTimedOut = fiber.memoizedState !== null;
3703+ const isTimedOut = suspenseState !== null;
36943704 if (!isTimedOut) {
36953705 newSuspenseNode.rects = measureInstance(newInstance);
36963706 }
@@ -3821,37 +3831,42 @@ export function attach(
38213831 // Modern Suspense path
38223832 const contentFiber = fiber.child;
38233833 if (contentFiber === null) {
3824- throw new Error(
3825- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3826- );
3827- }
3828-
3829- trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
3830-
3831- const fallbackFiber = contentFiber.sibling;
3834+ const suspenseState = fiber.memoizedState;
3835+ if (suspenseState === null || suspenseState.dehydrated === null) {
3836+ throw new Error(
3837+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3838+ );
3839+ }
3840+ // This Suspense Fiber is still dehydrated. It won't have any children
3841+ // until hydration.
3842+ } else {
3843+ trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
38323844
3833- // First update only the Offscreen boundary. I.e. the main content.
3834- mountVirtualChildrenRecursively(
3835- contentFiber,
3836- fallbackFiber,
3837- traceNearestHostComponentUpdate,
3838- 0, // first level
3839- );
3845+ const fallbackFiber = contentFiber.sibling;
38403846
3841- // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3842- // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3843- // Since the fallback conceptually blocks the parent.
3844- reconcilingParentSuspenseNode = stashedSuspenseParent;
3845- previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3846- remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3847- shouldPopSuspenseNode = false;
3848- if (fallbackFiber !== null) {
3847+ // First update only the Offscreen boundary. I.e. the main content.
38493848 mountVirtualChildrenRecursively(
3849+ contentFiber,
38503850 fallbackFiber,
3851- null,
38523851 traceNearestHostComponentUpdate,
38533852 0, // first level
38543853 );
3854+
3855+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3856+ // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3857+ // Since the fallback conceptually blocks the parent.
3858+ reconcilingParentSuspenseNode = stashedSuspenseParent;
3859+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3860+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3861+ shouldPopSuspenseNode = false;
3862+ if (fallbackFiber !== null) {
3863+ mountVirtualChildrenRecursively(
3864+ fallbackFiber,
3865+ null,
3866+ traceNearestHostComponentUpdate,
3867+ 0, // first level
3868+ );
3869+ }
38553870 }
38563871 } else {
38573872 if (fiber.child !== null) {
@@ -4768,68 +4783,84 @@ export function attach(
47684783 const prevContentFiber = prevFiber.child;
47694784 const nextContentFiber = nextFiber.child;
47704785 if (nextContentFiber === null || prevContentFiber === null) {
4771- throw new Error(
4772- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
4773- );
4774- }
4775- const prevFallbackFiber = prevContentFiber.sibling;
4776- const nextFallbackFiber = nextContentFiber.sibling;
4786+ const previousSuspenseState = prevFiber.memoizedState;
4787+ const nextSuspenseState = nextFiber.memoizedState;
4788+ if (
4789+ previousSuspenseState === null ||
4790+ previousSuspenseState.dehydrated === null ||
4791+ nextSuspenseState === null ||
4792+ nextSuspenseState.dehydrated === null
4793+ ) {
4794+ throw new Error(
4795+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4796+ );
4797+ }
4798+ // This Suspense Fiber is still dehydrated. It won't have any children
4799+ // until hydration.
4800+ } else {
4801+ const prevFallbackFiber = prevContentFiber.sibling;
4802+ const nextFallbackFiber = nextContentFiber.sibling;
47774803
4778- if ((prevFiber.stateNode === null) !== (nextFiber.stateNode === null)) {
4779- trackThrownPromisesFromRetryCache(
4780- fiberInstance.suspenseNode,
4781- nextFiber.stateNode,
4782- );
4783- }
4804+ if (
4805+ (prevFiber.stateNode === null) !==
4806+ (nextFiber.stateNode === null)
4807+ ) {
4808+ trackThrownPromisesFromRetryCache(
4809+ fiberInstance.suspenseNode,
4810+ nextFiber.stateNode,
4811+ );
4812+ }
47844813
4785- // First update only the Offscreen boundary. I.e. the main content.
4786- updateFlags |= updateVirtualChildrenRecursively(
4787- nextContentFiber,
4788- nextFallbackFiber,
4789- prevContentFiber,
4790- traceNearestHostComponentUpdate,
4791- 0,
4792- );
4814+ // First update only the Offscreen boundary. I.e. the main content.
4815+ updateFlags |= updateVirtualChildrenRecursively(
4816+ nextContentFiber,
4817+ nextFallbackFiber,
4818+ prevContentFiber,
4819+ traceNearestHostComponentUpdate,
4820+ 0,
4821+ );
47934822
4794- shouldMeasureSuspenseNode = false;
4795- if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4796- const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4797- const fallbackStashedSuspensePrevious =
4798- previouslyReconciledSiblingSuspenseNode;
4799- const fallbackStashedSuspenseRemaining =
4800- remainingReconcilingChildrenSuspenseNodes;
4801- // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4802- // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4803- // Since the fallback conceptually blocks the parent.
4804- reconcilingParentSuspenseNode = stashedSuspenseParent;
4805- previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4806- remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4807- try {
4808- if (nextFallbackFiber === null) {
4809- unmountRemainingChildren();
4810- } else {
4811- updateFlags |= updateVirtualChildrenRecursively(
4812- nextFallbackFiber,
4813- null,
4814- prevFallbackFiber,
4815- traceNearestHostComponentUpdate,
4816- 0,
4817- );
4818- }
4819- } finally {
4820- reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4821- previouslyReconciledSiblingSuspenseNode =
4822- fallbackStashedSuspensePrevious;
4823+ shouldMeasureSuspenseNode = false;
4824+ if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4825+ const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4826+ const fallbackStashedSuspensePrevious =
4827+ previouslyReconciledSiblingSuspenseNode;
4828+ const fallbackStashedSuspenseRemaining =
4829+ remainingReconcilingChildrenSuspenseNodes;
4830+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4831+ // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4832+ // Since the fallback conceptually blocks the parent.
4833+ reconcilingParentSuspenseNode = stashedSuspenseParent;
4834+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
48234835 remainingReconcilingChildrenSuspenseNodes =
4824- fallbackStashedSuspenseRemaining;
4836+ stashedSuspenseRemaining;
4837+ try {
4838+ if (nextFallbackFiber === null) {
4839+ unmountRemainingChildren();
4840+ } else {
4841+ updateFlags |= updateVirtualChildrenRecursively(
4842+ nextFallbackFiber,
4843+ null,
4844+ prevFallbackFiber,
4845+ traceNearestHostComponentUpdate,
4846+ 0,
4847+ );
4848+ }
4849+ } finally {
4850+ reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4851+ previouslyReconciledSiblingSuspenseNode =
4852+ fallbackStashedSuspensePrevious;
4853+ remainingReconcilingChildrenSuspenseNodes =
4854+ fallbackStashedSuspenseRemaining;
4855+ }
4856+ }
4857+ if (nextFiber.memoizedState === null) {
4858+ // Measure this Suspense node in case it changed. We don't update the rect while
4859+ // we're inside a disconnected subtree nor if we are the Suspense boundary that
4860+ // is suspended. This lets us keep the rectangle of the displayed content while
4861+ // we're suspended to visualize the resulting state.
4862+ shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48254863 }
4826- }
4827- if (nextFiber.memoizedState === null) {
4828- // Measure this Suspense node in case it changed. We don't update the rect while
4829- // we're inside a disconnected subtree nor if we are the Suspense boundary that
4830- // is suspended. This lets us keep the rectangle of the displayed content while
4831- // we're suspended to visualize the resulting state.
4832- shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48334864 }
48344865 } else {
48354866 // Common case: Primary -> Primary.
0 commit comments