Skip to content

Commit 6471d10

Browse files
committed
[Fizz] Track postpones in fallbacks (#27421)
This fixes so that you can postpone in a fallback. This postpones the parent boundary. I track the fallbacks in a separate replay node so that when we resume, we can replay the fallback itself and finish the fallback and then possibly later the content. By doing this we also ensure we don't complete the parent too early since now it has a render task on it. There is one case that this surfaces that isn't limited to prerender/resume but also render/hydrateRoot. I left todos in the tests for this. If you postpone in a fallback, and suspend in the content but eventually don't postpone in the content then we should be able to just skip postponing since the content rendered and we no longer need the fallback. This is a bit of a weird edge case though since fallbacks are supposed to be very minimal. This happens because in both cases the fallback starts rendering early as soon as the content suspends. This also ensures that the parent doesn't complete early by increasing the blocking tasks. Unfortunately, the fallback will irreversibly postpone its parent boundary as soon as it hits a postpone. When you suspend, the same thing happens but we typically deal with this by doing a "soft" abort on the fallback since we don't need it anymore which unblocks the parent boundary. We can't do that with postpone right now though since it's considered a terminal state. I think I'll just leave this as is for now since it's an edge case but it's an annoying exception in the model. Makes me feel I haven't quite nailed it just yet. DiffTrain build for [bff6be8](bff6be8)
1 parent dff0ebb commit 6471d10

8 files changed

+1392
-987
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7f6201889e8e628eeb53e05d8850ddffa3c2e74a
1+
bff6be8eb1d77980c13f3e01be63cb813a377058

compiled/facebook-www/ReactDOMServer-dev.classic.js

Lines changed: 116 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-classic-8314963c";
22+
var ReactVersion = "18.3.0-www-classic-87fc393f";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -9994,7 +9994,7 @@ function pingTask(request, task) {
99949994
}
99959995
}
99969996

9997-
function createSuspenseBoundary(request, fallbackAbortableTasks, keyPath) {
9997+
function createSuspenseBoundary(request, fallbackAbortableTasks) {
99989998
return {
99999999
status: PENDING,
1000010000
rootSegmentID: -1,
@@ -10005,7 +10005,8 @@ function createSuspenseBoundary(request, fallbackAbortableTasks, keyPath) {
1000510005
fallbackAbortableTasks: fallbackAbortableTasks,
1000610006
errorDigest: null,
1000710007
resources: createBoundaryResources(),
10008-
keyPath: keyPath
10008+
trackedContentKeyPath: null,
10009+
trackedFallbackNode: null
1000910010
};
1001010011
}
1001110012

@@ -10271,7 +10272,12 @@ function renderSuspenseBoundary(request, someTask, keyPath, props) {
1027110272
var fallback = props.fallback;
1027210273
var content = props.children;
1027310274
var fallbackAbortSet = new Set();
10274-
var newBoundary = createSuspenseBoundary(request, fallbackAbortSet, keyPath);
10275+
var newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
10276+
10277+
if (request.trackedPostpones !== null) {
10278+
newBoundary.trackedContentKeyPath = keyPath;
10279+
}
10280+
1027510281
var insertionIndex = parentSegment.chunks.length; // The children of the boundary segment is actually the fallback.
1027610282

1027710283
var boundarySegment = createPendingSegment(
@@ -10363,6 +10369,25 @@ function renderSuspenseBoundary(request, someTask, keyPath, props) {
1036310369
task.blockedBoundary = parentBoundary;
1036410370
task.blockedSegment = parentSegment;
1036510371
task.keyPath = prevKeyPath;
10372+
}
10373+
10374+
var fallbackKeyPath = [keyPath[0], "Suspense Fallback", keyPath[2]];
10375+
var trackedPostpones = request.trackedPostpones;
10376+
10377+
if (trackedPostpones !== null) {
10378+
// We create a detached replay node to track any postpones inside the fallback.
10379+
var fallbackReplayNode = [fallbackKeyPath[1], fallbackKeyPath[2], [], null];
10380+
trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);
10381+
10382+
if (newBoundary.status === POSTPONED) {
10383+
// This must exist now.
10384+
var boundaryReplayNode = trackedPostpones.workingMap.get(keyPath);
10385+
boundaryReplayNode[4] = fallbackReplayNode;
10386+
} else {
10387+
// We might not inject it into the postponed tree, unless the content actually
10388+
// postpones too. We need to keep track of it until that happpens.
10389+
newBoundary.trackedFallbackNode = fallbackReplayNode;
10390+
}
1036610391
} // We create suspended task for the fallback because we don't want to actually work
1036710392
// on it yet in case we finish the main content, so we queue for later.
1036810393

@@ -10373,8 +10398,8 @@ function renderSuspenseBoundary(request, someTask, keyPath, props) {
1037310398
-1,
1037410399
parentBoundary,
1037510400
boundarySegment,
10376-
fallbackAbortSet, // TODO: Should distinguish key path of fallback and primary tasks
10377-
keyPath,
10401+
fallbackAbortSet,
10402+
fallbackKeyPath,
1037810403
task.formatContext,
1037910404
task.legacyContext,
1038010405
task.context,
@@ -10397,19 +10422,18 @@ function replaySuspenseBoundary(
1039710422
props,
1039810423
id,
1039910424
childNodes,
10400-
childSlots
10425+
childSlots,
10426+
fallbackNodes,
10427+
fallbackSlots
1040110428
) {
1040210429
pushBuiltInComponentStackInDEV(task, "Suspense");
1040310430
var prevKeyPath = task.keyPath;
1040410431
var previousReplaySet = task.replay;
1040510432
var parentBoundary = task.blockedBoundary;
1040610433
var content = props.children;
10434+
var fallback = props.fallback;
1040710435
var fallbackAbortSet = new Set();
10408-
var resumedBoundary = createSuspenseBoundary(
10409-
request,
10410-
fallbackAbortSet,
10411-
task.keyPath
10412-
);
10436+
var resumedBoundary = createSuspenseBoundary(request, fallbackAbortSet);
1041310437
resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender.
1041410438

1041510439
resumedBoundary.rootSegmentID = id; // We can reuse the current context and task to render the content immediately without
@@ -10438,14 +10462,6 @@ function replaySuspenseBoundary(
1043810462
renderNode(request, task, content, -1);
1043910463
}
1044010464

10441-
if (
10442-
resumedBoundary.pendingTasks === 0 &&
10443-
resumedBoundary.status === PENDING
10444-
) {
10445-
resumedBoundary.status = COMPLETED;
10446-
request.completedBoundaries.push(resumedBoundary);
10447-
}
10448-
1044910465
if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
1045010466
throw new Error(
1045110467
"Couldn't find all resumable slots by key/index during replaying. " +
@@ -10454,6 +10470,19 @@ function replaySuspenseBoundary(
1045410470
}
1045510471

1045610472
task.replay.pendingTasks--;
10473+
10474+
if (
10475+
resumedBoundary.pendingTasks === 0 &&
10476+
resumedBoundary.status === PENDING
10477+
) {
10478+
resumedBoundary.status = COMPLETED;
10479+
request.completedBoundaries.push(resumedBoundary); // This must have been the last segment we were waiting on. This boundary is now complete.
10480+
// Therefore we won't need the fallback. We early return so that we don't have to create
10481+
// the fallback.
10482+
10483+
popComponentStackInDEV(task);
10484+
return;
10485+
}
1045710486
} catch (error) {
1045810487
resumedBoundary.status = CLIENT_RENDERED;
1045910488
var errorDigest;
@@ -10484,7 +10513,66 @@ function replaySuspenseBoundary(
1048410513
task.blockedBoundary = parentBoundary;
1048510514
task.replay = previousReplaySet;
1048610515
task.keyPath = prevKeyPath;
10487-
} // TODO: Should this be in the finally?
10516+
}
10517+
10518+
var fallbackKeyPath = [keyPath[0], "Suspense Fallback", keyPath[2]];
10519+
var suspendedFallbackTask; // We create suspended task for the fallback because we don't want to actually work
10520+
// on it yet in case we finish the main content, so we queue for later.
10521+
10522+
if (typeof fallbackSlots === "number") {
10523+
// Resuming directly in the fallback.
10524+
var resumedSegment = createPendingSegment(
10525+
request,
10526+
0,
10527+
null,
10528+
task.formatContext,
10529+
false,
10530+
false
10531+
);
10532+
resumedSegment.id = fallbackSlots;
10533+
resumedSegment.parentFlushed = true;
10534+
suspendedFallbackTask = createRenderTask(
10535+
request,
10536+
null,
10537+
fallback,
10538+
-1,
10539+
parentBoundary,
10540+
resumedSegment,
10541+
fallbackAbortSet,
10542+
fallbackKeyPath,
10543+
task.formatContext,
10544+
task.legacyContext,
10545+
task.context,
10546+
task.treeContext
10547+
);
10548+
} else {
10549+
var fallbackReplay = {
10550+
nodes: fallbackNodes,
10551+
slots: fallbackSlots,
10552+
pendingTasks: 0
10553+
};
10554+
suspendedFallbackTask = createReplayTask(
10555+
request,
10556+
null,
10557+
fallbackReplay,
10558+
fallback,
10559+
-1,
10560+
parentBoundary,
10561+
fallbackAbortSet,
10562+
fallbackKeyPath,
10563+
task.formatContext,
10564+
task.legacyContext,
10565+
task.context,
10566+
task.treeContext
10567+
);
10568+
}
10569+
10570+
{
10571+
suspendedFallbackTask.componentStack = task.componentStack;
10572+
} // TODO: This should be queued at a separate lower priority queue so that we only work
10573+
// on preparing fallbacks if we don't have any more main content to task on.
10574+
10575+
request.pingedTasks.push(suspendedFallbackTask); // TODO: Should this be in the finally?
1048810576

1048910577
popComponentStackInDEV(task);
1049010578
}
@@ -11392,9 +11480,11 @@ function replayElement(
1139211480
task,
1139311481
keyPath,
1139411482
props,
11395-
node[4],
11483+
node[5],
1139611484
node[2],
11397-
node[3]
11485+
node[3],
11486+
node[4] === null ? [] : node[4][2],
11487+
node[4] === null ? null : node[4][3]
1139811488
);
1139911489
} // We finished rendering this node, so now we can consume this
1140011490
// slot. This must happen after in case we rerender this task.
@@ -12104,11 +12194,7 @@ function abortRemainingSuspenseBoundary(
1210412194
error,
1210512195
errorDigest
1210612196
) {
12107-
var resumedBoundary = createSuspenseBoundary(
12108-
request,
12109-
new Set(),
12110-
null // The keyPath doesn't matter at this point so we don't bother rebuilding it.
12111-
);
12197+
var resumedBoundary = createSuspenseBoundary(request, new Set());
1211212198
resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender.
1211312199

1211412200
resumedBoundary.rootSegmentID = rootSegmentID;
@@ -12163,7 +12249,7 @@ function abortRemainingReplayNodes(
1216312249
);
1216412250
} else {
1216512251
var boundaryNode = node;
12166-
var rootSegmentID = boundaryNode[4];
12252+
var rootSegmentID = boundaryNode[5];
1216712253
abortRemainingSuspenseBoundary(
1216812254
request,
1216912255
rootSegmentID,
@@ -12950,9 +13036,7 @@ function flushCompletedQueues(request, destination) {
1295013036
destination,
1295113037
request.resumableState,
1295213038
request.renderState,
12953-
request.allPendingTasks === 0 &&
12954-
(request.trackedPostpones === null ||
12955-
request.trackedPostpones.workingMap.size === 0)
13039+
request.allPendingTasks === 0 && request.trackedPostpones === null
1295613040
);
1295713041
}
1295813042

0 commit comments

Comments
 (0)