diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 9b5e2455002af..2b9f3b156261b 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -6988,4 +6988,100 @@ describe('ReactDOMFizzServer', () => {
,
);
});
+
+ // @gate enablePostpone
+ it('can discover new suspense boundaries in the resume', async () => {
+ let prerendering = true;
+ let resolveA;
+ const promiseA = new Promise(r => (resolveA = r));
+ let resolveB;
+ const promiseB = new Promise(r => (resolveB = r));
+
+ function WaitA() {
+ return React.use(promiseA);
+ }
+ function WaitB() {
+ return React.use(promiseB);
+ }
+ function Postpone() {
+ if (prerendering) {
+ React.unstable_postpone();
+ }
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ function App() {
+ return (
+
+ );
+ }
+
+ const prerendered = await ReactDOMFizzStatic.prerenderToNodeStream();
+ expect(prerendered.postponed).not.toBe(null);
+
+ prerendering = false;
+
+ // Create a separate stream so it doesn't close the writable. I.e. simple concat.
+ const preludeWritable = new Stream.PassThrough();
+ preludeWritable.setEncoding('utf8');
+ preludeWritable.on('data', chunk => {
+ writable.write(chunk);
+ });
+
+ await act(() => {
+ prerendered.prelude.pipe(preludeWritable);
+ });
+
+ const resumed = await ReactDOMFizzServer.resumeToPipeableStream(
+ ,
+ JSON.parse(JSON.stringify(prerendered.postponed)),
+ );
+
+ expect(getVisibleChildren(container)).toEqual(Loading...
);
+
+ // Read what we've completed so far
+ await act(() => {
+ resumed.pipe(writable);
+ });
+
+ // Still blocked
+ expect(getVisibleChildren(container)).toEqual(Loading...
);
+
+ // Resolve the first promise, this unblocks the inner boundary
+ await act(() => {
+ resolveA('Hello');
+ });
+
+ // Still blocked
+ expect(getVisibleChildren(container)).toEqual(Loading...
);
+
+ // Resolve the second promise, this unblocks the outer boundary
+ await act(() => {
+ resolveB('World');
+ });
+
+ expect(getVisibleChildren(container)).toEqual(
+
+
+
+ {'Hello'}
+ {'World'}
+
+
+
,
+ );
+ });
});
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 7a0011445bf95..951b3e07930a2 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -487,7 +487,7 @@ export function resumeRequest(
progressiveChunkSize: postponedState.progressiveChunkSize,
status: OPEN,
fatalError: null,
- nextSegmentId: 0,
+ nextSegmentId: postponedState.nextSegmentId,
allPendingTasks: 0,
pendingRootTasks: 0,
completedRootSegment: null,
@@ -1019,11 +1019,7 @@ function replaySuspenseBoundary(
}
try {
// We use the safe form because we don't handle suspending here. Only error handling.
- if (typeof childSlots === 'number') {
- resumeNode(request, task, childSlots, content, -1);
- } else {
- renderNode(request, task, content, -1);
- }
+ renderNode(request, task, content, -1);
if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
throw new Error(
"Couldn't find all resumable slots by key/index during replaying. " +
@@ -1086,56 +1082,27 @@ function replaySuspenseBoundary(
const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
- let suspendedFallbackTask;
// We create suspended task for the fallback because we don't want to actually work
// on it yet in case we finish the main content, so we queue for later.
- if (typeof fallbackSlots === 'number') {
- // Resuming directly in the fallback.
- const resumedSegment = createPendingSegment(
- request,
- 0,
- null,
- task.formatContext,
- false,
- false,
- );
- resumedSegment.id = fallbackSlots;
- resumedSegment.parentFlushed = true;
- suspendedFallbackTask = createRenderTask(
- request,
- null,
- fallback,
- -1,
- parentBoundary,
- resumedSegment,
- fallbackAbortSet,
- fallbackKeyPath,
- task.formatContext,
- task.legacyContext,
- task.context,
- task.treeContext,
- );
- } else {
- const fallbackReplay = {
- nodes: fallbackNodes,
- slots: fallbackSlots,
- pendingTasks: 0,
- };
- suspendedFallbackTask = createReplayTask(
- request,
- null,
- fallbackReplay,
- fallback,
- -1,
- parentBoundary,
- fallbackAbortSet,
- fallbackKeyPath,
- task.formatContext,
- task.legacyContext,
- task.context,
- task.treeContext,
- );
- }
+ const fallbackReplay = {
+ nodes: fallbackNodes,
+ slots: fallbackSlots,
+ pendingTasks: 0,
+ };
+ const suspendedFallbackTask = createReplayTask(
+ request,
+ null,
+ fallbackReplay,
+ fallback,
+ -1,
+ parentBoundary,
+ fallbackAbortSet,
+ fallbackKeyPath,
+ task.formatContext,
+ task.legacyContext,
+ task.context,
+ task.treeContext,
+ );
if (__DEV__) {
suspendedFallbackTask.componentStack = task.componentStack;
}
@@ -1965,50 +1932,6 @@ function resumeNode(
}
}
-function resumeElement(
- request: Request,
- task: ReplayTask,
- keyPath: KeyNode,
- segmentId: number,
- prevThenableState: ThenableState | null,
- type: any,
- props: Object,
- ref: any,
-): void {
- const prevReplay = task.replay;
- const blockedBoundary = task.blockedBoundary;
- const resumedSegment = createPendingSegment(
- request,
- 0,
- null,
- task.formatContext,
- false,
- false,
- );
- resumedSegment.id = segmentId;
- resumedSegment.parentFlushed = true;
- try {
- // Convert the current ReplayTask to a RenderTask.
- const renderTask: RenderTask = (task: any);
- renderTask.replay = null;
- renderTask.blockedSegment = resumedSegment;
- renderElement(request, task, keyPath, prevThenableState, type, props, ref);
- resumedSegment.status = COMPLETED;
- if (blockedBoundary === null) {
- request.completedRootSegment = resumedSegment;
- } else {
- queueCompletedSegment(blockedBoundary, resumedSegment);
- if (blockedBoundary.parentFlushed) {
- request.partialBoundaries.push(blockedBoundary);
- }
- }
- } finally {
- // Restore to a ReplayTask.
- task.replay = prevReplay;
- task.blockedSegment = null;
- }
-}
-
function replayElement(
request: Request,
task: ReplayTask,
@@ -2045,29 +1968,15 @@ function replayElement(
const childSlots = node[3];
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
try {
- if (typeof childSlots === 'number') {
- // Matched a resumable element.
- resumeElement(
- request,
- task,
- keyPath,
- childSlots,
- prevThenableState,
- type,
- props,
- ref,
- );
- } else {
- renderElement(
- request,
- task,
- keyPath,
- prevThenableState,
- type,
- props,
- ref,
- );
- }
+ renderElement(
+ request,
+ task,
+ keyPath,
+ prevThenableState,
+ type,
+ props,
+ ref,
+ );
if (
task.replay.pendingTasks === 1 &&
task.replay.nodes.length > 0
@@ -2215,6 +2124,12 @@ function renderNodeDestructiveImpl(
node: ReactNodeList,
childIndex: number,
): void {
+ if (task.replay !== null && typeof task.replay.slots === 'number') {
+ // TODO: Figure out a cheaper place than this hot path to do this check.
+ const resumeSegmentID = task.replay.slots;
+ resumeNode(request, task, resumeSegmentID, node, childIndex);
+ return;
+ }
// Stash the node we're working on. We'll pick up from this task in case
// something suspends.
task.node = node;