diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
index 6873031207fd5..1a241364fbb9c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
@@ -493,6 +493,47 @@ describe('ReactDOMFizzStaticBrowser', () => {
// TODO: expect(getVisibleChildren(container)).toEqual(
Hello
);
});
+ // @gate enablePostpone
+ it('supports postponing in lazy in prerender and resuming later', async () => {
+ let prerendering = true;
+ const Hole = React.lazy(async () => {
+ React.unstable_postpone();
+ });
+
+ function Postpone() {
+ return 'Hello';
+ }
+
+ function App() {
+ return (
+
+
+ Hi
+ {prerendering ? Hole : }
+
+
+ );
+ }
+
+ const prerendered = await ReactDOMFizzStatic.prerender();
+ expect(prerendered.postponed).not.toBe(null);
+
+ prerendering = false;
+
+ const resumed = await ReactDOMFizzServer.resume(
+ ,
+ prerendered.postponed,
+ );
+
+ await readIntoContainer(prerendered.prelude);
+
+ expect(getVisibleChildren(container)).toEqual(Loading...
);
+
+ await readIntoContainer(resumed);
+
+ // TODO: expect(getVisibleChildren(container)).toEqual(Hello
);
+ });
+
// @gate enablePostpone
it('only emits end tags once when resuming', async () => {
let prerendering = true;
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 318b3cac95af0..42c5bc54405dd 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -186,8 +186,7 @@ type ResumableNode =
| ResumableParentNode
| [
2, // RESUME_SEGMENT
- string | null /* name */,
- string | number /* key */,
+ number /* index */,
number /* segment id */,
];
@@ -220,6 +219,7 @@ type SuspenseBoundary = {
export type Task = {
node: ReactNodeList,
+ childIndex: number,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment, // the segment we'll write to
@@ -1632,6 +1632,7 @@ function renderNodeDestructiveImpl(
// Stash the node we're working on. We'll pick up from this task in case
// something suspends.
task.node = node;
+ task.childIndex = childIndex;
// Handle object types
if (typeof node === 'object' && node !== null) {
@@ -1809,18 +1810,45 @@ function renderChildrenArray(
for (let i = 0; i < totalChildren; i++) {
const node = children[i];
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
- if (isArray(node) || getIteratorFn(node)) {
- // Nested arrays behave like a "fragment node" which is keyed.
- // Therefore we need to add the current index as a parent key.
+
+ // Nested arrays behave like a "fragment node" which is keyed.
+ // Therefore we need to add the current index as a parent key.
+ // We first check if the nested nodes are arrays or iterables.
+
+ if (isArray(node)) {
const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, '', childIndex];
- renderNode(request, task, node, i);
+ renderChildrenArray(request, task, node, i);
task.keyPath = prevKeyPath;
- } else {
- // We need to use the non-destructive form so that we can safely pop back
- // up and render the sibling if something suspends.
- renderNode(request, task, node, i);
+ continue;
}
+
+ const iteratorFn = getIteratorFn(node);
+ if (iteratorFn) {
+ if (__DEV__) {
+ validateIterable(node, iteratorFn);
+ }
+ const iterator = iteratorFn.call(node);
+ if (iterator) {
+ let step = iterator.next();
+ if (!step.done) {
+ const prevKeyPath = task.keyPath;
+ task.keyPath = [task.keyPath, '', childIndex];
+ const nestedChildren = [];
+ do {
+ nestedChildren.push(step.value);
+ step = iterator.next();
+ } while (!step.done);
+ renderChildrenArray(request, task, nestedChildren, i);
+ task.keyPath = prevKeyPath;
+ }
+ continue;
+ }
+ }
+
+ // We need to use the non-destructive form so that we can safely pop back
+ // up and render the sibling if something suspends.
+ renderNode(request, task, node, i);
}
// Because this context is always set right before rendering every child, we
// only need to reset it to the previous value at the very end.
@@ -1831,6 +1859,7 @@ function trackPostpone(
request: Request,
trackedPostpones: PostponedHoles,
task: Task,
+ childIndex: number,
segment: Segment,
): void {
segment.status = POSTPONED;
@@ -1862,7 +1891,7 @@ function trackPostpone(
boundary.id,
];
trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
- addToResumableParent(boundaryNode, boundaryKeyPath, trackedPostpones);
+ addToResumableParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
}
const keyPath = task.keyPath;
@@ -1872,12 +1901,7 @@ function trackPostpone(
);
}
- const segmentNode: ResumableNode = [
- RESUME_SEGMENT,
- keyPath[1],
- keyPath[2],
- segment.id,
- ];
+ const segmentNode: ResumableNode = [RESUME_SEGMENT, childIndex, segment.id];
addToResumableParent(segmentNode, keyPath, trackedPostpones);
}
@@ -1941,6 +1965,7 @@ function spawnNewSuspendedTask(
task.context,
task.treeContext,
);
+ newTask.childIndex = task.childIndex;
if (__DEV__) {
if (task.componentStack !== null) {
@@ -2035,7 +2060,13 @@ function renderNode(
task,
postponeInstance.message,
);
- trackPostpone(request, trackedPostpones, task, postponedSegment);
+ trackPostpone(
+ request,
+ trackedPostpones,
+ task,
+ childIndex,
+ postponedSegment,
+ );
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
@@ -2328,7 +2359,13 @@ function retryTask(request: Request, task: Task): void {
const prevThenableState = task.thenableState;
task.thenableState = null;
- renderNodeDestructive(request, task, prevThenableState, task.node, 0);
+ renderNodeDestructive(
+ request,
+ task,
+ prevThenableState,
+ task.node,
+ task.childIndex,
+ );
pushSegmentFinale(
segment.chunks,
request.renderState,
@@ -2377,8 +2414,15 @@ function retryTask(request: Request, task: Task): void {
task.abortSet.delete(task);
const postponeInstance: Postpone = (x: any);
logPostpone(request, postponeInstance.message);
- trackPostpone(request, trackedPostpones, task, segment);
+ trackPostpone(
+ request,
+ trackedPostpones,
+ task,
+ task.childIndex,
+ segment,
+ );
finishedTask(request, task.blockedBoundary, segment);
+ return;
}
}
task.abortSet.delete(task);
@@ -2975,10 +3019,9 @@ export function getResumableState(request: Request): ResumableState {
function addToResumableParent(
node: ResumableNode,
- keyPath: KeyNode,
+ parentKeyPath: Root | KeyNode,
trackedPostpones: PostponedHoles,
): void {
- const parentKeyPath = keyPath[0];
if (parentKeyPath === null) {
trackedPostpones.root.push(node);
} else {
@@ -2992,7 +3035,7 @@ function addToResumableParent(
([]: Array),
]: ResumableParentNode);
workingMap.set(parentKeyPath, parentNode);
- addToResumableParent(parentNode, parentKeyPath, trackedPostpones);
+ addToResumableParent(parentNode, parentKeyPath[0], trackedPostpones);
}
parentNode[3].push(node);
}