765
// The incoming update might unblock the current render. Interrupt the
766
// current attempt and restart from the top.
767
prepareFreshStack(root, NoLanes);
768
+ const didAttemptEntireTree = false;
769
markRootSuspended(
770
root,
771
workInProgressRootRenderLanes,
772
workInProgressDeferredLane,
772
- workInProgressRootDidSkipSuspendedSiblings,
773
+ didAttemptEntireTree,
774
);
775
}
776
833
// effect of interrupting the current render and switching to the update.
834
// TODO: Make sure this doesn't override pings that happen while we've
835
// already started rendering.
836
+ const didAttemptEntireTree = false;
837
markRootSuspended(
838
root,
839
workInProgressRootRenderLanes,
840
workInProgressDeferredLane,
839
- workInProgressRootDidSkipSuspendedSiblings,
841
+ didAttemptEntireTree,
842
);
843
}
844
}
900
// for too long ("expired" work, to prevent starvation), or we're in
901
// sync-updates-by-default mode.
902
const shouldTimeSlice =
901
- !forceSync &&
902
- !includesBlockingLane(lanes) &&
903
- !includesExpiredLane(root, lanes);
903
+ (!forceSync &&
904
+ !includesBlockingLane(lanes) &&
905
+ !includesExpiredLane(root, lanes)) ||
906
+ // If we're prerendering, then we should use the concurrent work loop
907
+ // even if the lanes are synchronous, so that prerendering never blocks
908
+ // the main thread.
909
+ // TODO: We should consider doing this whenever a sync lane is suspended,
910
+ // even for regular pings.
911
+ (enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));
912
+
913
let exitStatus = shouldTimeSlice
914
? renderRootConcurrent(root, lanes)
906
- : renderRootSync(root, lanes);
915
+ : renderRootSync(root, lanes, true);
916
908
- if (exitStatus !== RootInProgress) {
917
+ do {
918
let renderWasConcurrent = shouldTimeSlice;
910
- do {
911
- if (exitStatus === RootDidNotComplete) {
912
- // The render unwound without completing the tree. This happens in special
913
- // cases where need to exit the current render without producing a
914
- // consistent tree or committing.
915
- markRootSuspended(
919
+ if (exitStatus === RootInProgress) {
920
+ // Render phase is still in progress.
921
+ if (
922
+ enableSiblingPrerendering &&
923
+ workInProgressRootIsPrerendering &&
924
+ !shouldTimeSlice
925
+ ) {
926
+ // We're in prerendering mode, but time slicing is not enabled. This
927
+ // happens when something suspends during a synchronous update. Exit the
928
+ // the work loop. When we resume, we'll use the concurrent work loop so
929
+ // that prerendering is non-blocking.
930
+ //
931
+ // Mark the root as suspended. Usually we do this at the end of the
932
+ // render phase, but we do it here so that we resume in
933
+ // prerendering mode.
934
+ // TODO: Consider always calling markRootSuspended immediately.
935
+ // Needs to be *after* we attach a ping listener, though.
936
+ const didAttemptEntireTree = false;
937
+ markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
938
+ }
939
+ break;
940
+ } else if (exitStatus === RootDidNotComplete) {
941
+ // The render unwound without completing the tree. This happens in special
942
+ // cases where need to exit the current render without producing a
943
+ // consistent tree or committing.
944
+ const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;
945
+ markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
946
+ } else {
947
+ // The render completed.
948
+
949
+ // Check if this render may have yielded to a concurrent event, and if so,
950
+ // confirm that any newly rendered stores are consistent.
951
+ // TODO: It's possible that even a concurrent render may never have yielded
952
+ // to the main thread, if it was fast enough, or if it expired. We could
953
+ // skip the consistency check in that case, too.
954
+ const finishedWork: Fiber = (root.current.alternate: any);
955
+ if (
956
+ renderWasConcurrent &&
957
+ !isRenderConsistentWithExternalStores(finishedWork)
958
+ ) {
959
+ // A store was mutated in an interleaved event. Render again,
960
+ // synchronously, to block further mutations.
961
+ exitStatus = renderRootSync(root, lanes, false);
962
+ // We assume the tree is now consistent because we didn't yield to any
963
+ // concurrent events.
964
+ renderWasConcurrent = false;
965
+ // Need to check the exit status again.
966
+ continue;
967
+ }
968
+
969
+ // Check if something threw
970
+ if (
971
+ (disableLegacyMode || root.tag !== LegacyRoot) &&
972
+ exitStatus === RootErrored
973
+ ) {
974
+ const lanesThatJustErrored = lanes;
975
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
976
root,
917
- lanes,
918
- NoLane,
919
- workInProgressRootDidSkipSuspendedSiblings,
977
+ lanesThatJustErrored,
978
);
921
- } else {
922
- // The render completed.
923
-
924
- // Check if this render may have yielded to a concurrent event, and if so,
925
- // confirm that any newly rendered stores are consistent.
926
- // TODO: It's possible that even a concurrent render may never have yielded
927
- // to the main thread, if it was fast enough, or if it expired. We could
928
- // skip the consistency check in that case, too.
929
- const finishedWork: Fiber = (root.current.alternate: any);
930
- if (
931
- renderWasConcurrent &&
932
- !isRenderConsistentWithExternalStores(finishedWork)
933
- ) {
934
- // A store was mutated in an interleaved event. Render again,
935
- // synchronously, to block further mutations.
936
- exitStatus = renderRootSync(root, lanes);
937
- // We assume the tree is now consistent because we didn't yield to any
938
- // concurrent events.
939
- renderWasConcurrent = false;
940
- // Need to check the exit status again.
941
- continue;
942
- }
943
-
944
- // Check if something threw
945
- if (
946
- (disableLegacyMode || root.tag !== LegacyRoot) &&
947
- exitStatus === RootErrored
948
- ) {
949
- const lanesThatJustErrored = lanes;
950
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
979
+ if (errorRetryLanes !== NoLanes) {
980
+ lanes = errorRetryLanes;
981
+ exitStatus = recoverFromConcurrentError(
982
root,
983
lanesThatJustErrored,
984
+ errorRetryLanes,
985
);
954
- if (errorRetryLanes !== NoLanes) {
955
- lanes = errorRetryLanes;
956
- exitStatus = recoverFromConcurrentError(
957
- root,
958
- lanesThatJustErrored,
959
- errorRetryLanes,
960
- );
961
- renderWasConcurrent = false;
962
- // Need to check the exit status again.
963
- if (exitStatus !== RootErrored) {
964
- // The root did not error this time. Restart the exit algorithm
965
- // from the beginning.
966
- // TODO: Refactor the exit algorithm to be less confusing. Maybe
967
- // more branches + recursion instead of a loop. I think the only
968
- // thing that causes it to be a loop is the RootDidNotComplete
969
- // check. If that's true, then we don't need a loop/recursion
970
- // at all.
971
- continue;
972
- } else {
973
- // The root errored yet again. Proceed to commit the tree.
974
- }
986
+ renderWasConcurrent = false;
987
+ // Need to check the exit status again.
988
+ if (exitStatus !== RootErrored) {
989
+ // The root did not error this time. Restart the exit algorithm
990
+ // from the beginning.
991
+ // TODO: Refactor the exit algorithm to be less confusing. Maybe
992
+ // more branches + recursion instead of a loop. I think the only
993
+ // thing that causes it to be a loop is the RootDidNotComplete
994
+ // check. If that's true, then we don't need a loop/recursion
995
+ // at all.
996
+ continue;
997
+ } else {
998
+ // The root errored yet again. Proceed to commit the tree.
999
}
1000
}
977
- if (exitStatus === RootFatalErrored) {
978
- prepareFreshStack(root, NoLanes);
979
- markRootSuspended(
980
- root,
981
- lanes,
982
- NoLane,
983
- workInProgressRootDidSkipSuspendedSiblings,
984
- );
985
- break;
986
- }
987
-
988
- // We now have a consistent tree. The next step is either to commit it,
989
- // or, if something suspended, wait to commit it after a timeout.
990
- finishConcurrentRender(root, exitStatus, finishedWork, lanes);
1001
}
992
- break;
993
- } while (true);
994
- }
1002
+ if (exitStatus === RootFatalErrored) {
1003
+ prepareFreshStack(root, NoLanes);
1004
+ // Since this is a fatal error, we're going to pretend we attempted
1005
+ // the entire tree, to avoid scheduling a prerender.
1006
+ const didAttemptEntireTree = true;
1007
+ markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
1008
+ break;
1009
+ }
1010
+
1011
+ // We now have a consistent tree. The next step is either to commit it,
1012
+ // or, if something suspended, wait to commit it after a timeout.
1013
+ finishConcurrentRender(root, exitStatus, finishedWork, lanes);
1014
+ }
1015
+ break;
1016
+ } while (true);
1017
1018
ensureRootIsScheduled(root);
1019
}
1046
rootWorkInProgress.flags |= ForceClientRender;
1047
}
1048
1027
- const exitStatus = renderRootSync(root, errorRetryLanes);
1049
+ const exitStatus = renderRootSync(root, errorRetryLanes, false);
1050
if (exitStatus !== RootErrored) {
1051
// Successfully finished rendering on retry
1052
1130
// This is a transition, so we should exit without committing a
1131
// placeholder and without scheduling a timeout. Delay indefinitely
1132
// until we receive more data.
1133
+ const didAttemptEntireTree =
1134
+ !workInProgressRootDidSkipSuspendedSiblings;
1135
markRootSuspended(
1136
root,
1137
lanes,
1138
workInProgressDeferredLane,
1115
- workInProgressRootDidSkipSuspendedSiblings,
1139
+ didAttemptEntireTree,
1140
);
1141
return;
1142
}
1192
1193
// Don't bother with a very short suspense time.
1194
if (msUntilTimeout > 10) {
1195
+ const didAttemptEntireTree =
1196
+ !workInProgressRootDidSkipSuspendedSiblings;
1197
markRootSuspended(
1198
root,
1199
lanes,
1200
workInProgressDeferredLane,
1175
- workInProgressRootDidSkipSuspendedSiblings,
1201
+ didAttemptEntireTree,
1202
);
1203
1204
const nextLanes = getNextLanes(root, NoLanes);
1312
completedRenderEndTime,
1313
),
1314
);
1289
- markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings);
1315
+ const didAttemptEntireTree = !didSkipSuspendedSiblings;
1316
+ markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
1317
return;
1318
}
1319
}
1436
root: FiberRoot,
1437
suspendedLanes: Lanes,
1438
spawnedLane: Lane,
1412
- didSkipSuspendedSiblings: boolean,
1439
+ didAttemptEntireTree: boolean,
1440
) {
1441
// When suspending, we should always exclude lanes that were pinged or (more
1442
// rarely, since we try to avoid it) updated during the render phase.
1445
suspendedLanes,
1446
workInProgressRootInterleavedUpdatedLanes,
1447
);
1421
- _markRootSuspended(
1422
- root,
1423
- suspendedLanes,
1424
- spawnedLane,
1425
- didSkipSuspendedSiblings,
1426
- );
1448
+ _markRootSuspended(root, suspendedLanes, spawnedLane, didAttemptEntireTree);
1449
}
1450
1451
export function flushRoot(root: FiberRoot, lanes: Lanes) {
1987
1988
if (
1989
!workInProgressRootDidSkipSuspendedSiblings &&
1968
- !includesBlockingLane(workInProgressRootRenderLanes)
1990
+ // Check if the root will be blocked from committing.
1991
+ // TODO: Consider aligning this better with the rest of the logic. Maybe
1992
+ // we should only set the exit status to RootSuspendedWithDelay if this
1993
+ // condition is true? And remove the equivalent checks elsewhere.
1994
+ (includesOnlyTransitions(workInProgressRootRenderLanes) ||
1995
+ getSuspenseHandler() === null)
1996
) {
1997
// This render may not have originally been scheduled as a prerender, but
1998
// something suspended inside the visible part of the tree, which means we
2018
// pinged or updated while we were rendering.
2019
// TODO: Consider unwinding immediately, using the
2020
// SuspendedOnHydration mechanism.
2021
+ const didAttemptEntireTree = false;
2022
markRootSuspended(
2023
workInProgressRoot,
2024
workInProgressRootRenderLanes,
2025
workInProgressDeferredLane,
1998
- workInProgressRootDidSkipSuspendedSiblings,
2026
+ didAttemptEntireTree,
2027
);
2028
}
2029
}
2053
// TODO: Over time, this function and renderRootConcurrent have become more
2054
// and more similar. Not sure it makes sense to maintain forked paths. Consider
2055
// unifying them again.
2028
-function renderRootSync(root: FiberRoot, lanes: Lanes) {
2056
+function renderRootSync(
2057
+ root: FiberRoot,
2058
+ lanes: Lanes,
2059
+ shouldYieldForPrerendering: boolean,
2060
+): RootExitStatus {
2061
const prevExecutionContext = executionContext;
2062
executionContext |= RenderContext;
2063
const prevDispatcher = pushDispatcher(root.containerInfo);
2097
}
2098
2099
let didSuspendInShell = false;
2100
+ let exitStatus = workInProgressRootExitStatus;
2101
outer: do {
2102
try {
2103
if (
2119
// Selective hydration. An update flowed into a dehydrated tree.
2120
// Interrupt the current render so the work loop can switch to the
2121
// hydration lane.
2122
+ // TODO: I think we might not need to reset the stack here; we can
2123
+ // just yield and reset the stack when we re-enter the work loop,
2124
+ // like normal.
2125
resetWorkInProgressStack();
2090
- workInProgressRootExitStatus = RootDidNotComplete;
2126
+ exitStatus = RootDidNotComplete;
2127
break outer;
2128
}
2129
case SuspendedOnImmediate:
2094
- case SuspendedOnData: {
2095
- if (!didSuspendInShell && getSuspenseHandler() === null) {
2130
+ case SuspendedOnData:
2131
+ case SuspendedOnDeprecatedThrowPromise: {
2132
+ if (getSuspenseHandler() === null) {
2133
didSuspendInShell = true;
2134
}
2098
- // Intentional fallthrough
2135
+ const reason = workInProgressSuspendedReason;
2136
+ workInProgressSuspendedReason = NotSuspended;
2137
+ workInProgressThrownValue = null;
2138
+ throwAndUnwindWorkLoop(root, unitOfWork, thrownValue, reason);
2139
+ if (
2140
+ enableSiblingPrerendering &&
2141
+ shouldYieldForPrerendering &&
2142
+ workInProgressRootIsPrerendering
2143
+ ) {
2144
+ // We've switched into prerendering mode. This implies that we
2145
+ // suspended outside of a Suspense boundary, which means this
2146
+ // render will be blocked from committing. Yield to the main
2147
+ // thread so we can switch to prerendering using the concurrent
2148
+ // work loop.
2149
+ exitStatus = RootInProgress;
2150
+ break outer;
2151
+ }
2152
+ break;
2153
}
2154
default: {
2155
// Unwind then continue with the normal work loop.
2162
}
2163
}
2164
workLoopSync();
2165
+ exitStatus = workInProgressRootExitStatus;
2166
break;
2167
} catch (thrownValue) {
2168
handleThrow(root, thrownValue);
2185
popDispatcher(prevDispatcher);
2186
popAsyncDispatcher(prevAsyncDispatcher);
2187
2133
- if (workInProgress !== null) {
2134
- // This is a sync render, so we should have finished the whole tree.
2135
- throw new Error(
2136
- 'Cannot commit an incomplete root. This error is likely caused by a ' +
2137
- 'bug in React. Please file an issue.',
2138
- );
2139
- }
2140
-
2188
if (__DEV__) {
2189
if (enableDebugTracing) {
2190
logRenderStopped();
2195
markRenderStopped();
2196
}
2197
2151
- // Set this to null to indicate there's no in-progress render.
2152
- workInProgressRoot = null;
2153
- workInProgressRootRenderLanes = NoLanes;
2198
+ if (workInProgress !== null) {
2199
+ // Did not complete the tree. This can happen if something suspended in
2200
+ // the shell.
2201
+ } else {
2202
+ // Normal case. We completed the whole tree.
2203
+
2204
+ // Set this to null to indicate there's no in-progress render.
2205
+ workInProgressRoot = null;
2206
+ workInProgressRootRenderLanes = NoLanes;
2207
2155
- // It's safe to process the queue now that the render phase is complete.
2156
- finishQueueingConcurrentUpdates();
2208
+ // It's safe to process the queue now that the render phase is complete.
2209
+ finishQueueingConcurrentUpdates();
2210
+ }
2211
2158
- return workInProgressRootExitStatus;
2212
+ return exitStatus;
2213
}
2214
2215
// The work loop is an extremely hot path. Tell Closure not to inline it.
2255
//
2256
// If we were previously in prerendering mode, check if we received any new
2257
// data during an interleaved event.
2204
- if (workInProgressRootIsPrerendering) {
2205
- workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
2206
- }
2258
+ workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
2259
}
2260
2261
if (__DEV__) {
3805
// the logic of whether or not a root suspends once it completes.
3806
// TODO: If we're rendering sync either due to Sync, Batched or expired,
3807
// we should probably never restart.
3808
+ // TODO: Attach different listeners depending on whether the listener was
3809
+ // attached during prerendering. Prerender pings should not interrupt
3810
+ // normal renders.
3811
3812
// If we're suspended with delay, or if it's a retry, we'll always suspend
3813
// so we can always restart.