@@ -759,6 +759,93 @@ function bubbleProperties(completedWork: Fiber) {
759759 return didBailout ;
760760}
761761
762+ function completeDehydratedSuspenseBoundary (
763+ current : Fiber | null ,
764+ workInProgress : Fiber ,
765+ nextState : SuspenseState | null ,
766+ ) : boolean {
767+ if (
768+ hasUnhydratedTailNodes ( ) &&
769+ ( workInProgress . mode & ConcurrentMode ) !== NoMode &&
770+ ( workInProgress . flags & DidCapture ) === NoFlags
771+ ) {
772+ warnIfUnhydratedTailNodes ( workInProgress ) ;
773+ resetHydrationState ( ) ;
774+ workInProgress . flags |= ForceClientRender | Incomplete | ShouldCapture ;
775+
776+ return false ;
777+ }
778+
779+ const wasHydrated = popHydrationState ( workInProgress ) ;
780+
781+ if ( nextState !== null && nextState . dehydrated !== null ) {
782+ // We might be inside a hydration state the first time we're picking up this
783+ // Suspense boundary, and also after we've reentered it for further hydration.
784+ if ( current === null ) {
785+ if ( ! wasHydrated ) {
786+ throw new Error (
787+ 'A dehydrated suspense component was completed without a hydrated node. ' +
788+ 'This is probably a bug in React.' ,
789+ ) ;
790+ }
791+ prepareToHydrateHostSuspenseInstance ( workInProgress ) ;
792+ bubbleProperties ( workInProgress ) ;
793+ if ( enableProfilerTimer ) {
794+ if ( ( workInProgress . mode & ProfileMode ) !== NoMode ) {
795+ const isTimedOutSuspense = nextState !== null ;
796+ if ( isTimedOutSuspense ) {
797+ // Don't count time spent in a timed out Suspense subtree as part of the base duration.
798+ const primaryChildFragment = workInProgress . child ;
799+ if ( primaryChildFragment !== null ) {
800+ // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
801+ workInProgress . treeBaseDuration -= ( ( primaryChildFragment . treeBaseDuration : any ) : number ) ;
802+ }
803+ }
804+ }
805+ }
806+ return false ;
807+ } else {
808+ // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
809+ // state since we're now exiting out of it. popHydrationState doesn't do that for us.
810+ resetHydrationState ( ) ;
811+ if ( ( workInProgress . flags & DidCapture ) === NoFlags ) {
812+ // This boundary did not suspend so it's now hydrated and unsuspended.
813+ workInProgress . memoizedState = null ;
814+ }
815+ // If nothing suspended, we need to schedule an effect to mark this boundary
816+ // as having hydrated so events know that they're free to be invoked.
817+ // It's also a signal to replay events and the suspense callback.
818+ // If something suspended, schedule an effect to attach retry listeners.
819+ // So we might as well always mark this.
820+ workInProgress . flags |= Update ;
821+ bubbleProperties ( workInProgress ) ;
822+ if ( enableProfilerTimer ) {
823+ if ( ( workInProgress . mode & ProfileMode ) !== NoMode ) {
824+ const isTimedOutSuspense = nextState !== null ;
825+ if ( isTimedOutSuspense ) {
826+ // Don't count time spent in a timed out Suspense subtree as part of the base duration.
827+ const primaryChildFragment = workInProgress . child ;
828+ if ( primaryChildFragment !== null ) {
829+ // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
830+ workInProgress . treeBaseDuration -= ( ( primaryChildFragment . treeBaseDuration : any ) : number ) ;
831+ }
832+ }
833+ }
834+ }
835+ return false ;
836+ }
837+ } else {
838+ // Successfully completed this tree. If this was a forced client render,
839+ // there may have been recoverable errors during first hydration
840+ // attempt. If so, add them to a queue so we can log them in the
841+ // commit phase.
842+ upgradeHydrationErrorsToRecoverable ( ) ;
843+
844+ // Fall through to normal Suspense path
845+ return true ;
846+ }
847+ }
848+
762849function completeWork (
763850 current : Fiber | null ,
764851 workInProgress : Fiber ,
@@ -996,80 +1083,35 @@ function completeWork(
9961083 popSuspenseContext ( workInProgress ) ;
9971084 const nextState : null | SuspenseState = workInProgress . memoizedState ;
9981085
1086+ // Special path for dehydrated boundaries. We may eventually move this
1087+ // to its own fiber type so that we can add other kinds of hydration
1088+ // boundaries that aren't associated with a Suspense tree. In anticipation
1089+ // of such a refactor, all the hydration logic is contained in
1090+ // this branch.
9991091 if (
1000- hasUnhydratedTailNodes ( ) &&
1001- ( workInProgress . mode & ConcurrentMode ) !== NoMode &&
1002- ( workInProgress . flags & DidCapture ) === NoFlags
1092+ current === null ||
1093+ ( current . memoizedState !== null &&
1094+ current . memoizedState . dehydrated !== null )
10031095 ) {
1004- warnIfUnhydratedTailNodes ( workInProgress ) ;
1005- resetHydrationState ( ) ;
1006- workInProgress . flags |= ForceClientRender | Incomplete | ShouldCapture ;
1007- return workInProgress ;
1008- }
1009- if ( nextState !== null && nextState . dehydrated !== null ) {
1010- // We might be inside a hydration state the first time we're picking up this
1011- // Suspense boundary, and also after we've reentered it for further hydration.
1012- const wasHydrated = popHydrationState ( workInProgress ) ;
1013- if ( current === null ) {
1014- if ( ! wasHydrated ) {
1015- throw new Error (
1016- 'A dehydrated suspense component was completed without a hydrated node. ' +
1017- 'This is probably a bug in React.' ,
1018- ) ;
1019- }
1020- prepareToHydrateHostSuspenseInstance ( workInProgress ) ;
1021- bubbleProperties ( workInProgress ) ;
1022- if ( enableProfilerTimer ) {
1023- if ( ( workInProgress . mode & ProfileMode ) !== NoMode ) {
1024- const isTimedOutSuspense = nextState !== null ;
1025- if ( isTimedOutSuspense ) {
1026- // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1027- const primaryChildFragment = workInProgress . child ;
1028- if ( primaryChildFragment !== null ) {
1029- // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
1030- workInProgress . treeBaseDuration -= ( ( primaryChildFragment . treeBaseDuration : any ) : number ) ;
1031- }
1032- }
1033- }
1034- }
1035- return null ;
1036- } else {
1037- // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
1038- // state since we're now exiting out of it. popHydrationState doesn't do that for us.
1039- resetHydrationState ( ) ;
1040- if ( ( workInProgress . flags & DidCapture ) === NoFlags ) {
1041- // This boundary did not suspend so it's now hydrated and unsuspended.
1042- workInProgress . memoizedState = null ;
1043- }
1044- // If nothing suspended, we need to schedule an effect to mark this boundary
1045- // as having hydrated so events know that they're free to be invoked.
1046- // It's also a signal to replay events and the suspense callback.
1047- // If something suspended, schedule an effect to attach retry listeners.
1048- // So we might as well always mark this.
1049- workInProgress . flags |= Update ;
1050- bubbleProperties ( workInProgress ) ;
1051- if ( enableProfilerTimer ) {
1052- if ( ( workInProgress . mode & ProfileMode ) !== NoMode ) {
1053- const isTimedOutSuspense = nextState !== null ;
1054- if ( isTimedOutSuspense ) {
1055- // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1056- const primaryChildFragment = workInProgress . child ;
1057- if ( primaryChildFragment !== null ) {
1058- // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
1059- workInProgress . treeBaseDuration -= ( ( primaryChildFragment . treeBaseDuration : any ) : number ) ;
1060- }
1061- }
1062- }
1096+ const fallthroughToNormalSuspensePath = completeDehydratedSuspenseBoundary (
1097+ current ,
1098+ workInProgress ,
1099+ nextState ,
1100+ ) ;
1101+ if ( ! fallthroughToNormalSuspensePath ) {
1102+ if ( workInProgress . flags & ShouldCapture ) {
1103+ // Special case. There were remaining unhydrated nodes. We treat
1104+ // this as a mismatch. Revert to client rendering.
1105+ return workInProgress ;
1106+ } else {
1107+ // Did not finish hydrating, either because this is the initial
1108+ // render or because something suspended.
1109+ return null ;
10631110 }
1064- return null ;
10651111 }
1066- }
10671112
1068- // Successfully completed this tree. If this was a forced client render,
1069- // there may have been recoverable errors during first hydration
1070- // attempt. If so, add them to a queue so we can log them in the
1071- // commit phase.
1072- upgradeHydrationErrorsToRecoverable ( ) ;
1113+ // Continue with the normal Suspense path.
1114+ }
10731115
10741116 if ( ( workInProgress . flags & DidCapture ) !== NoFlags ) {
10751117 // Something suspended. Re-render with the fallback children.
@@ -1086,13 +1128,9 @@ function completeWork(
10861128 }
10871129
10881130 const nextDidTimeout = nextState !== null ;
1089- let prevDidTimeout = false ;
1090- if ( current === null ) {
1091- popHydrationState ( workInProgress ) ;
1092- } else {
1093- const prevState : null | SuspenseState = current . memoizedState ;
1094- prevDidTimeout = prevState !== null ;
1095- }
1131+ const prevDidTimeout =
1132+ current !== null &&
1133+ ( current . memoizedState : null | SuspenseState ) !== null ;
10961134
10971135 if ( enableCache && nextDidTimeout ) {
10981136 const offscreenFiber : Fiber = ( workInProgress . child : any ) ;
0 commit comments