@@ -759,6 +759,93 @@ function bubbleProperties(completedWork: Fiber) {
759
759
return didBailout ;
760
760
}
761
761
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
+
762
849
function completeWork (
763
850
current : Fiber | null ,
764
851
workInProgress : Fiber ,
@@ -996,80 +1083,35 @@ function completeWork(
996
1083
popSuspenseContext ( workInProgress ) ;
997
1084
const nextState : null | SuspenseState = workInProgress . memoizedState ;
998
1085
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.
999
1091
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 )
1003
1095
) {
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 ;
1063
1110
}
1064
- return null ;
1065
1111
}
1066
- }
1067
1112
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
+ }
1073
1115
1074
1116
if ( ( workInProgress . flags & DidCapture ) !== NoFlags ) {
1075
1117
// Something suspended. Re-render with the fallback children.
@@ -1086,13 +1128,9 @@ function completeWork(
1086
1128
}
1087
1129
1088
1130
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 ;
1096
1134
1097
1135
if ( enableCache && nextDidTimeout ) {
1098
1136
const offscreenFiber : Fiber = ( workInProgress . child : any ) ;
0 commit comments