@@ -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,14 +3692,15 @@ export function attach(
36323692 newSuspenseNode.rects = measureInstance(newInstance);
36333693 }
36343694 } else {
3635- const contentFiber = fiber.child ;
3636- if (contentFiber === null ) {
3637- const suspenseState = fiber.memoizedState ;
3638- if (suspenseState === null || suspenseState.dehydrated === null) {
3695+ const hydrated = isFiberHydrated( fiber) ;
3696+ if (hydrated ) {
3697+ const contentFiber = fiber.child ;
3698+ if (contentFiber === null) {
36393699 throw new Error(
36403700 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36413701 );
36423702 }
3703+ } else {
36433704 // This Suspense Fiber is still dehydrated. It won't have any children
36443705 // until hydration.
36453706 }
@@ -3689,17 +3750,19 @@ export function attach(
36893750 newSuspenseNode.rects = measureInstance(newInstance);
36903751 }
36913752 } else {
3692- const contentFiber = fiber.child ;
3693- const suspenseState = fiber.memoizedState;
3694- if ( contentFiber === null) {
3695- if (suspenseState === null || suspenseState.dehydrated === null) {
3753+ const hydrated = isFiberHydrated( fiber) ;
3754+ if (hydrated) {
3755+ const contentFiber = fiber.child;
3756+ if (contentFiber === null) {
36963757 throw new Error(
36973758 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36983759 );
36993760 }
3761+ } else {
37003762 // This Suspense Fiber is still dehydrated. It won't have any children
37013763 // until hydration.
37023764 }
3765+ const suspenseState = fiber.memoizedState;
37033766 const isTimedOut = suspenseState !== null;
37043767 if (!isTimedOut) {
37053768 newSuspenseNode.rects = measureInstance(newInstance);
@@ -3830,43 +3893,26 @@ export function attach(
38303893 ) {
38313894 // Modern Suspense path
38323895 const contentFiber = fiber.child;
3833- if (contentFiber === null) {
3834- const suspenseState = fiber.memoizedState;
3835- if (suspenseState === null || suspenseState.dehydrated === null) {
3896+ const hydrated = isFiberHydrated(fiber);
3897+ if (hydrated) {
3898+ if (contentFiber === null) {
38363899 throw new Error(
38373900 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
38383901 );
38393902 }
3840- // This Suspense Fiber is still dehydrated. It won't have any children
3841- // until hydration.
3842- } else {
3843- trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
38443903
3845- const fallbackFiber = contentFiber.sibling ;
3904+ trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode) ;
38463905
3847- // First update only the Offscreen boundary. I.e. the main content.
3848- mountVirtualChildrenRecursively(
3906+ mountSuspenseChildrenRecursively(
38493907 contentFiber,
3850- fallbackFiber,
38513908 traceNearestHostComponentUpdate,
3852- 0, // first level
3909+ stashedSuspenseParent,
3910+ stashedSuspensePrevious,
3911+ stashedSuspenseRemaining,
38533912 );
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- }
3913+ } else {
3914+ // This Suspense Fiber is still dehydrated. It won't have any children
3915+ // until hydration.
38703916 }
38713917 } else {
38723918 if (fiber.child !== null) {
@@ -4520,6 +4566,63 @@ export function attach(
45204566 );
45214567 }
45224568
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+
45234626 // Returns whether closest unfiltered fiber parent needs to reset its child list.
45244627 function updateFiberRecursively(
45254628 fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
@@ -4780,87 +4883,67 @@ export function attach(
47804883 fiberInstance.suspenseNode !== null
47814884 ) {
47824885 // Modern Suspense path
4886+ const suspenseNode = fiberInstance.suspenseNode;
47834887 const prevContentFiber = prevFiber.child;
47844888 const nextContentFiber = nextFiber.child;
4785- if (nextContentFiber === null || prevContentFiber === null) {
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- ) {
4889+ const previousHydrated = isFiberHydrated(prevFiber);
4890+ const nextHydrated = isFiberHydrated(nextFiber);
4891+ if (previousHydrated && nextHydrated) {
4892+ if (nextContentFiber === null || prevContentFiber === null) {
47944893 throw new Error(
47954894 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
47964895 );
47974896 }
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;
48034897
48044898 if (
48054899 (prevFiber.stateNode === null) !==
48064900 (nextFiber.stateNode === null)
48074901 ) {
48084902 trackThrownPromisesFromRetryCache(
4809- fiberInstance. suspenseNode,
4903+ suspenseNode,
48104904 nextFiber.stateNode,
48114905 );
48124906 }
48134907
4814- // First update only the Offscreen boundary. I.e. the main content.
4815- updateFlags |= updateVirtualChildrenRecursively (
4908+ shouldMeasureSuspenseNode = false;
4909+ updateFlags |= updateSuspenseChildrenRecursively (
48164910 nextContentFiber,
4817- nextFallbackFiber,
48184911 prevContentFiber,
48194912 traceNearestHostComponentUpdate,
4820- 0,
4913+ stashedSuspenseParent,
4914+ stashedSuspensePrevious,
4915+ stashedSuspenseRemaining,
48214916 );
4822-
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;
4835- remainingReconcilingChildrenSuspenseNodes =
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- }
48574917 if (nextFiber.memoizedState === null) {
48584918 // Measure this Suspense node in case it changed. We don't update the rect while
48594919 // we're inside a disconnected subtree nor if we are the Suspense boundary that
48604920 // is suspended. This lets us keep the rectangle of the displayed content while
48614921 // we're suspended to visualize the resulting state.
48624922 shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48634923 }
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+ }
4930+
4931+ trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);
4932+
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.
48644947 }
48654948 } else {
48664949 // Common case: Primary -> Primary.
0 commit comments