@@ -1543,6 +1543,22 @@ export function attach(
15431543 return Array.from(knownEnvironmentNames);
15441544 }
15451545
1546+ function isFiberHydrated(fiber: Fiber): boolean {
1547+ if (OffscreenComponent === -1) {
1548+ throw new Error('not implemented for legacy suspense');
1549+ }
1550+ switch (fiber.tag) {
1551+ case HostRoot:
1552+ const rootState = fiber.memoizedState;
1553+ return !rootState.isDehydrated;
1554+ case SuspenseComponent:
1555+ const suspenseState = fiber.memoizedState;
1556+ return suspenseState === null || suspenseState.dehydrated === null;
1557+ default:
1558+ throw new Error('not implemented for work tag ' + fiber.tag);
1559+ }
1560+ }
1561+
15461562 function shouldFilterVirtual(
15471563 data: ReactComponentInfo,
15481564 secondaryEnv: null | string,
@@ -3610,6 +3626,50 @@ export function attach(
36103626 );
36113627 }
36123628
3629+ function mountSuspenseChildrenRecursively(
3630+ contentFiber: Fiber,
3631+ traceNearestHostComponentUpdate: boolean,
3632+ stashedSuspenseParent: SuspenseNode | null,
3633+ stashedSuspensePrevious: SuspenseNode | null,
3634+ stashedSuspenseRemaining: SuspenseNode | null,
3635+ ) {
3636+ const fallbackFiber = contentFiber.sibling;
3637+
3638+ // First update only the Offscreen boundary. I.e. the main content.
3639+ mountVirtualChildrenRecursively(
3640+ contentFiber,
3641+ fallbackFiber,
3642+ traceNearestHostComponentUpdate,
3643+ 0, // first level
3644+ );
3645+
3646+ if (fallbackFiber !== null) {
3647+ const fallbackStashedSuspenseParent = stashedSuspenseParent;
3648+ const fallbackStashedSuspensePrevious = stashedSuspensePrevious;
3649+ const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining;
3650+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3651+ // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3652+ // Since the fallback conceptually blocks the parent.
3653+ reconcilingParentSuspenseNode = stashedSuspenseParent;
3654+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3655+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3656+ try {
3657+ mountVirtualChildrenRecursively(
3658+ fallbackFiber,
3659+ null,
3660+ traceNearestHostComponentUpdate,
3661+ 0, // first level
3662+ );
3663+ } finally {
3664+ reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
3665+ previouslyReconciledSiblingSuspenseNode =
3666+ fallbackStashedSuspensePrevious;
3667+ remainingReconcilingChildrenSuspenseNodes =
3668+ fallbackStashedSuspenseRemaining;
3669+ }
3670+ }
3671+ }
3672+
36133673 function mountFiberRecursively(
36143674 fiber: Fiber,
36153675 traceNearestHostComponentUpdate: boolean,
@@ -3632,11 +3692,17 @@ export function attach(
36323692 newSuspenseNode.rects = measureInstance(newInstance);
36333693 }
36343694 } else {
3635- const contentFiber = fiber.child;
3636- if (contentFiber === null) {
3637- throw new Error(
3638- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3639- );
3695+ const hydrated = isFiberHydrated(fiber);
3696+ if (hydrated) {
3697+ const contentFiber = fiber.child;
3698+ if (contentFiber === null) {
3699+ throw new Error(
3700+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3701+ );
3702+ }
3703+ } else {
3704+ // This Suspense Fiber is still dehydrated. It won't have any children
3705+ // until hydration.
36403706 }
36413707 const isTimedOut = fiber.memoizedState !== null;
36423708 if (!isTimedOut) {
@@ -3684,13 +3750,20 @@ export function attach(
36843750 newSuspenseNode.rects = measureInstance(newInstance);
36853751 }
36863752 } else {
3687- const contentFiber = fiber.child;
3688- if (contentFiber === null) {
3689- throw new Error(
3690- 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3691- );
3753+ const hydrated = isFiberHydrated(fiber);
3754+ if (hydrated) {
3755+ const contentFiber = fiber.child;
3756+ if (contentFiber === null) {
3757+ throw new Error(
3758+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3759+ );
3760+ }
3761+ } else {
3762+ // This Suspense Fiber is still dehydrated. It won't have any children
3763+ // until hydration.
36923764 }
3693- const isTimedOut = fiber.memoizedState !== null;
3765+ const suspenseState = fiber.memoizedState;
3766+ const isTimedOut = suspenseState !== null;
36943767 if (!isTimedOut) {
36953768 newSuspenseNode.rects = measureInstance(newInstance);
36963769 }
@@ -3820,38 +3893,26 @@ export function attach(
38203893 ) {
38213894 // Modern Suspense path
38223895 const contentFiber = fiber.child;
3823- 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;
3896+ const hydrated = isFiberHydrated(fiber);
3897+ if (hydrated) {
3898+ if (contentFiber === null) {
3899+ throw new Error(
3900+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3901+ );
3902+ }
38323903
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- );
3904+ trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
38403905
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) {
3849- mountVirtualChildrenRecursively(
3850- fallbackFiber,
3851- null,
3906+ mountSuspenseChildrenRecursively(
3907+ contentFiber,
38523908 traceNearestHostComponentUpdate,
3853- 0, // first level
3909+ stashedSuspenseParent,
3910+ stashedSuspensePrevious,
3911+ stashedSuspenseRemaining,
38543912 );
3913+ } else {
3914+ // This Suspense Fiber is still dehydrated. It won't have any children
3915+ // until hydration.
38553916 }
38563917 } else {
38573918 if (fiber.child !== null) {
@@ -4505,6 +4566,63 @@ export function attach(
45054566 );
45064567 }
45074568
4569+ function updateSuspenseChildrenRecursively(
4570+ nextContentFiber: Fiber,
4571+ prevContentFiber: Fiber,
4572+ traceNearestHostComponentUpdate: boolean,
4573+ stashedSuspenseParent: null | SuspenseNode,
4574+ stashedSuspensePrevious: null | SuspenseNode,
4575+ stashedSuspenseRemaining: null | SuspenseNode,
4576+ ): number {
4577+ let updateFlags = NoUpdate;
4578+ const prevFallbackFiber = prevContentFiber.sibling;
4579+ const nextFallbackFiber = nextContentFiber.sibling;
4580+
4581+ // First update only the Offscreen boundary. I.e. the main content.
4582+ updateFlags |= updateVirtualChildrenRecursively(
4583+ nextContentFiber,
4584+ nextFallbackFiber,
4585+ prevContentFiber,
4586+ traceNearestHostComponentUpdate,
4587+ 0,
4588+ );
4589+
4590+ if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4591+ const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4592+ const fallbackStashedSuspensePrevious =
4593+ previouslyReconciledSiblingSuspenseNode;
4594+ const fallbackStashedSuspenseRemaining =
4595+ remainingReconcilingChildrenSuspenseNodes;
4596+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4597+ // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4598+ // Since the fallback conceptually blocks the parent.
4599+ reconcilingParentSuspenseNode = stashedSuspenseParent;
4600+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4601+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4602+ try {
4603+ if (nextFallbackFiber === null) {
4604+ unmountRemainingChildren();
4605+ } else {
4606+ updateFlags |= updateVirtualChildrenRecursively(
4607+ nextFallbackFiber,
4608+ null,
4609+ prevFallbackFiber,
4610+ traceNearestHostComponentUpdate,
4611+ 0,
4612+ );
4613+ }
4614+ } finally {
4615+ reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4616+ previouslyReconciledSiblingSuspenseNode =
4617+ fallbackStashedSuspensePrevious;
4618+ remainingReconcilingChildrenSuspenseNodes =
4619+ fallbackStashedSuspenseRemaining;
4620+ }
4621+ }
4622+
4623+ return updateFlags;
4624+ }
4625+
45084626 // Returns whether closest unfiltered fiber parent needs to reset its child list.
45094627 function updateFiberRecursively(
45104628 fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
@@ -4765,71 +4883,67 @@ export function attach(
47654883 fiberInstance.suspenseNode !== null
47664884 ) {
47674885 // Modern Suspense path
4886+ const suspenseNode = fiberInstance.suspenseNode;
47684887 const prevContentFiber = prevFiber.child;
47694888 const nextContentFiber = nextFiber.child;
4770- 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;
4889+ const previousHydrated = isFiberHydrated(prevFiber);
4890+ const nextHydrated = isFiberHydrated(nextFiber);
4891+ if (previousHydrated && nextHydrated) {
4892+ if (nextContentFiber === null || prevContentFiber === null) {
4893+ throw new Error(
4894+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4895+ );
4896+ }
47774897
4778- if ((prevFiber.stateNode === null) !== (nextFiber.stateNode === null)) {
4779- trackThrownPromisesFromRetryCache(
4780- fiberInstance.suspenseNode,
4781- nextFiber.stateNode,
4898+ if (
4899+ (prevFiber.stateNode === null) !==
4900+ (nextFiber.stateNode === null)
4901+ ) {
4902+ trackThrownPromisesFromRetryCache(
4903+ suspenseNode,
4904+ nextFiber.stateNode,
4905+ );
4906+ }
4907+
4908+ shouldMeasureSuspenseNode = false;
4909+ updateFlags |= updateSuspenseChildrenRecursively(
4910+ nextContentFiber,
4911+ prevContentFiber,
4912+ traceNearestHostComponentUpdate,
4913+ stashedSuspenseParent,
4914+ stashedSuspensePrevious,
4915+ stashedSuspenseRemaining,
47824916 );
4783- }
4917+ if (nextFiber.memoizedState === null) {
4918+ // Measure this Suspense node in case it changed. We don't update the rect while
4919+ // we're inside a disconnected subtree nor if we are the Suspense boundary that
4920+ // is suspended. This lets us keep the rectangle of the displayed content while
4921+ // we're suspended to visualize the resulting state.
4922+ shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
4923+ }
4924+ } else if (!previousHydrated && nextHydrated) {
4925+ if (nextContentFiber === null) {
4926+ throw new Error(
4927+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4928+ );
4929+ }
47844930
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- );
4931+ trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);
47934932
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- remainingReconcilingChildrenSuspenseNodes =
4824- fallbackStashedSuspenseRemaining;
4825- }
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;
4933+ mountSuspenseChildrenRecursively(
4934+ nextContentFiber,
4935+ traceNearestHostComponentUpdate,
4936+ stashedSuspenseParent,
4937+ stashedSuspensePrevious,
4938+ stashedSuspenseRemaining,
4939+ );
4940+ } else if (previousHydrated && !nextHydrated) {
4941+ throw new Error(
4942+ 'Encountered a dehydrated Suspense boundary that was previously hydrated.',
4943+ );
4944+ } else {
4945+ // This Suspense Fiber is still dehydrated. It won't have any children
4946+ // until hydration.
48334947 }
48344948 } else {
48354949 // Common case: Primary -> Primary.
0 commit comments