Skip to content

Commit 3d0367e

Browse files
committed
Extract hydration logic in complete phase, too
Same as previous step but for the complete phase. This is a separate commit to make bisecting easier in case something breaks. The logic is very subtle but mostly all I've done is extract it to another function.
1 parent ff65114 commit 3d0367e

File tree

2 files changed

+228
-152
lines changed

2 files changed

+228
-152
lines changed

packages/react-reconciler/src/ReactFiberCompleteWork.new.js

Lines changed: 114 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
762849
function 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

Comments
 (0)