@@ -1534,6 +1534,22 @@ export function attach(
15341534 return Array.from(knownEnvironmentNames);
15351535 }
15361536
1537+ function isFiberHydrated(fiber: Fiber): boolean {
1538+ if (OffscreenComponent === -1) {
1539+ throw new Error('not implemented for legacy suspense');
1540+ }
1541+ switch (fiber.tag) {
1542+ case HostRoot:
1543+ const rootState = fiber.memoizedState;
1544+ return !rootState.isDehydrated;
1545+ case SuspenseComponent:
1546+ const suspenseState = fiber.memoizedState;
1547+ return suspenseState === null || suspenseState.dehydrated === null;
1548+ default:
1549+ throw new Error('not implemented for work tag ' + fiber.tag);
1550+ }
1551+ }
1552+
15371553 function shouldFilterVirtual(
15381554 data: ReactComponentInfo,
15391555 secondaryEnv: null | string,
@@ -3579,6 +3595,50 @@ export function attach(
35793595 );
35803596 }
35813597
3598+ function mountSuspenseChildrenRecursively(
3599+ contentFiber: Fiber,
3600+ traceNearestHostComponentUpdate: boolean,
3601+ stashedSuspenseParent: SuspenseNode | null,
3602+ stashedSuspensePrevious: SuspenseNode | null,
3603+ stashedSuspenseRemaining: SuspenseNode | null,
3604+ ) {
3605+ const fallbackFiber = contentFiber.sibling;
3606+
3607+ // First update only the Offscreen boundary. I.e. the main content.
3608+ mountVirtualChildrenRecursively(
3609+ contentFiber,
3610+ fallbackFiber,
3611+ traceNearestHostComponentUpdate,
3612+ 0, // first level
3613+ );
3614+
3615+ if (fallbackFiber !== null) {
3616+ const fallbackStashedSuspenseParent = stashedSuspenseParent;
3617+ const fallbackStashedSuspensePrevious = stashedSuspensePrevious;
3618+ const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining;
3619+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3620+ // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3621+ // Since the fallback conceptually blocks the parent.
3622+ reconcilingParentSuspenseNode = stashedSuspenseParent;
3623+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3624+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3625+ try {
3626+ mountVirtualChildrenRecursively(
3627+ fallbackFiber,
3628+ null,
3629+ traceNearestHostComponentUpdate,
3630+ 0, // first level
3631+ );
3632+ } finally {
3633+ reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
3634+ previouslyReconciledSiblingSuspenseNode =
3635+ fallbackStashedSuspensePrevious;
3636+ remainingReconcilingChildrenSuspenseNodes =
3637+ fallbackStashedSuspenseRemaining;
3638+ }
3639+ }
3640+ }
3641+
35823642 function mountFiberRecursively(
35833643 fiber: Fiber,
35843644 traceNearestHostComponentUpdate: boolean,
@@ -3601,14 +3661,15 @@ export function attach(
36013661 newSuspenseNode.rects = measureInstance(newInstance);
36023662 }
36033663 } else {
3604- const contentFiber = fiber.child ;
3605- if (contentFiber === null ) {
3606- const suspenseState = fiber.memoizedState ;
3607- if (suspenseState === null || suspenseState.dehydrated === null) {
3664+ const hydrated = isFiberHydrated( fiber) ;
3665+ if (hydrated ) {
3666+ const contentFiber = fiber.child ;
3667+ if (contentFiber === null) {
36083668 throw new Error(
36093669 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36103670 );
36113671 }
3672+ } else {
36123673 // This Suspense Fiber is still dehydrated. It won't have any children
36133674 // until hydration.
36143675 }
@@ -3658,17 +3719,19 @@ export function attach(
36583719 newSuspenseNode.rects = measureInstance(newInstance);
36593720 }
36603721 } else {
3661- const contentFiber = fiber.child ;
3662- const suspenseState = fiber.memoizedState;
3663- if ( contentFiber === null) {
3664- if (suspenseState === null || suspenseState.dehydrated === null) {
3722+ const hydrated = isFiberHydrated( fiber) ;
3723+ if (hydrated) {
3724+ const contentFiber = fiber.child;
3725+ if (contentFiber === null) {
36653726 throw new Error(
36663727 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36673728 );
36683729 }
3730+ } else {
36693731 // This Suspense Fiber is still dehydrated. It won't have any children
36703732 // until hydration.
36713733 }
3734+ const suspenseState = fiber.memoizedState;
36723735 const isTimedOut = suspenseState !== null;
36733736 if (!isTimedOut) {
36743737 newSuspenseNode.rects = measureInstance(newInstance);
@@ -3796,41 +3859,24 @@ export function attach(
37963859 ) {
37973860 // Modern Suspense path
37983861 const contentFiber = fiber.child;
3799- if (contentFiber === null) {
3800- const suspenseState = fiber.memoizedState;
3801- if (suspenseState === null || suspenseState.dehydrated === null) {
3862+ const hydrated = isFiberHydrated(fiber);
3863+ if (hydrated) {
3864+ if (contentFiber === null) {
38023865 throw new Error(
38033866 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
38043867 );
38053868 }
3806- // This Suspense Fiber is still dehydrated. It won't have any children
3807- // until hydration.
3808- } else {
3809- const fallbackFiber = contentFiber.sibling;
38103869
3811- // First update only the Offscreen boundary. I.e. the main content.
3812- mountVirtualChildrenRecursively(
3870+ mountSuspenseChildrenRecursively(
38133871 contentFiber,
3814- fallbackFiber,
38153872 traceNearestHostComponentUpdate,
3816- 0, // first level
3873+ stashedSuspenseParent,
3874+ stashedSuspensePrevious,
3875+ stashedSuspenseRemaining,
38173876 );
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- }
3877+ } else {
3878+ // This Suspense Fiber is still dehydrated. It won't have any children
3879+ // until hydration.
38343880 }
38353881 } else {
38363882 if (fiber.child !== null) {
@@ -4484,6 +4530,63 @@ export function attach(
44844530 );
44854531 }
44864532
4533+ function updateSuspenseChildrenRecursively(
4534+ nextContentFiber: Fiber,
4535+ prevContentFiber: Fiber,
4536+ traceNearestHostComponentUpdate: boolean,
4537+ stashedSuspenseParent: null | SuspenseNode,
4538+ stashedSuspensePrevious: null | SuspenseNode,
4539+ stashedSuspenseRemaining: null | SuspenseNode,
4540+ ): number {
4541+ let updateFlags = NoUpdate;
4542+ const prevFallbackFiber = prevContentFiber.sibling;
4543+ const nextFallbackFiber = nextContentFiber.sibling;
4544+
4545+ // First update only the Offscreen boundary. I.e. the main content.
4546+ updateFlags |= updateVirtualChildrenRecursively(
4547+ nextContentFiber,
4548+ nextFallbackFiber,
4549+ prevContentFiber,
4550+ traceNearestHostComponentUpdate,
4551+ 0,
4552+ );
4553+
4554+ if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4555+ const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4556+ const fallbackStashedSuspensePrevious =
4557+ previouslyReconciledSiblingSuspenseNode;
4558+ const fallbackStashedSuspenseRemaining =
4559+ remainingReconcilingChildrenSuspenseNodes;
4560+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4561+ // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4562+ // Since the fallback conceptually blocks the parent.
4563+ reconcilingParentSuspenseNode = stashedSuspenseParent;
4564+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4565+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4566+ try {
4567+ if (nextFallbackFiber === null) {
4568+ unmountRemainingChildren();
4569+ } else {
4570+ updateFlags |= updateVirtualChildrenRecursively(
4571+ nextFallbackFiber,
4572+ null,
4573+ prevFallbackFiber,
4574+ traceNearestHostComponentUpdate,
4575+ 0,
4576+ );
4577+ }
4578+ } finally {
4579+ reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4580+ previouslyReconciledSiblingSuspenseNode =
4581+ fallbackStashedSuspensePrevious;
4582+ remainingReconcilingChildrenSuspenseNodes =
4583+ fallbackStashedSuspenseRemaining;
4584+ }
4585+ }
4586+
4587+ return updateFlags;
4588+ }
4589+
44874590 // Returns whether closest unfiltered fiber parent needs to reset its child list.
44884591 function updateFiberRecursively(
44894592 fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
@@ -4734,75 +4837,51 @@ export function attach(
47344837 // Modern Suspense path
47354838 const prevContentFiber = prevFiber.child;
47364839 const nextContentFiber = nextFiber.child;
4737- if (nextContentFiber === null || prevContentFiber === null) {
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- ) {
4840+ const previousHydrated = isFiberHydrated(prevFiber);
4841+ const nextHydrated = isFiberHydrated(nextFiber);
4842+ if (previousHydrated && nextHydrated) {
4843+ if (nextContentFiber === null || prevContentFiber === null) {
47464844 throw new Error(
47474845 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
47484846 );
47494847 }
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;
47554848
4756- // First update only the Offscreen boundary. I.e. the main content.
4757- updateFlags |= updateVirtualChildrenRecursively (
4849+ shouldMeasureSuspenseNode = false;
4850+ updateFlags |= updateSuspenseChildrenRecursively (
47584851 nextContentFiber,
4759- nextFallbackFiber,
47604852 prevContentFiber,
47614853 traceNearestHostComponentUpdate,
4762- 0,
4854+ stashedSuspenseParent,
4855+ stashedSuspensePrevious,
4856+ stashedSuspenseRemaining,
47634857 );
4764-
4765- shouldMeasureSuspenseNode = false;
4766- if (prevFallbackFiber !== null || 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;
4777- remainingReconcilingChildrenSuspenseNodes =
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- }
47994858 if (nextFiber.memoizedState === null) {
48004859 // Measure this Suspense node in case it changed. We don't update the rect while
48014860 // we're inside a disconnected subtree nor if we are the Suspense boundary that
48024861 // is suspended. This lets us keep the rectangle of the displayed content while
48034862 // we're suspended to visualize the resulting state.
48044863 shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48054864 }
4865+ } else if (!previousHydrated && nextHydrated) {
4866+ if (nextContentFiber === null) {
4867+ throw new Error(
4868+ 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4869+ );
4870+ }
4871+ mountSuspenseChildrenRecursively(
4872+ nextContentFiber,
4873+ traceNearestHostComponentUpdate,
4874+ stashedSuspenseParent,
4875+ stashedSuspensePrevious,
4876+ stashedSuspenseRemaining,
4877+ );
4878+ } else if (previousHydrated && !nextHydrated) {
4879+ throw new Error(
4880+ 'Encountered a dehydrated Suspense boundary that was previously hydrated.',
4881+ );
4882+ } else {
4883+ // This Suspense Fiber is still dehydrated. It won't have any children
4884+ // until hydration.
48064885 }
48074886 } else {
48084887 // Common case: Primary -> Primary.
0 commit comments