diff --git a/packages/react-dom/npm/static.browser.js b/packages/react-dom/npm/static.browser.js
index 6d3f52b0e6..ddfe2b2089 100644
--- a/packages/react-dom/npm/static.browser.js
+++ b/packages/react-dom/npm/static.browser.js
@@ -9,3 +9,4 @@ if (process.env.NODE_ENV === 'production') {
exports.version = s.version;
exports.prerender = s.prerender;
+exports.resumeAndPrerender = s.resumeAndPrerender;
diff --git a/packages/react-dom/npm/static.edge.js b/packages/react-dom/npm/static.edge.js
index de57d7a4c0..ff770374b3 100644
--- a/packages/react-dom/npm/static.edge.js
+++ b/packages/react-dom/npm/static.edge.js
@@ -9,3 +9,4 @@ if (process.env.NODE_ENV === 'production') {
exports.version = s.version;
exports.prerender = s.prerender;
+exports.resumeAndPrerender = s.resumeAndPrerender;
diff --git a/packages/react-dom/npm/static.node.js b/packages/react-dom/npm/static.node.js
index 0a7cc8dfd7..5dc47d472b 100644
--- a/packages/react-dom/npm/static.node.js
+++ b/packages/react-dom/npm/static.node.js
@@ -9,3 +9,4 @@ if (process.env.NODE_ENV === 'production') {
exports.version = s.version;
exports.prerenderToNodeStream = s.prerenderToNodeStream;
+exports.resumeAndPrerenderToNodeStream = s.resumeAndPrerenderToNodeStream;
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
index 357ef1dcb4..1512e1d4c7 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
@@ -1758,4 +1758,90 @@ describe('ReactDOMFizzStaticBrowser', () => {
await readIntoContainer(dynamic);
expect(getVisibleChildren(container)).toEqual('hello');
});
+
+ // @gate enableHalt
+ it('can resume render of a prerender', async () => {
+ const errors = [];
+
+ let resolveA;
+ const promiseA = new Promise(r => (resolveA = r));
+ let resolveB;
+ const promiseB = new Promise(r => (resolveB = r));
+
+ async function ComponentA() {
+ await promiseA;
+ return (
+
+
+
+ );
+ }
+
+ async function ComponentB() {
+ await promiseB;
+ return 'Hello';
+ }
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ const controller = new AbortController();
+ let pendingResult;
+ await serverAct(async () => {
+ pendingResult = ReactDOMFizzStatic.prerender(, {
+ signal: controller.signal,
+ onError(x) {
+ errors.push(x.message);
+ },
+ });
+ });
+
+ controller.abort();
+
+ const prerendered = await pendingResult;
+ const postponedState = JSON.stringify(prerendered.postponed);
+
+ await readIntoContainer(prerendered.prelude);
+ expect(getVisibleChildren(container)).toEqual('Loading A');
+
+ await resolveA();
+
+ expect(prerendered.postponed).not.toBe(null);
+
+ const controller2 = new AbortController();
+ await serverAct(async () => {
+ pendingResult = ReactDOMFizzStatic.resumeAndPrerender(
+ ,
+ JSON.parse(postponedState),
+ {
+ signal: controller2.signal,
+ onError(x) {
+ errors.push(x.message);
+ },
+ },
+ );
+ });
+
+ controller2.abort();
+
+ const prerendered2 = await pendingResult;
+ const postponedState2 = JSON.stringify(prerendered2.postponed);
+
+ await readIntoContainer(prerendered2.prelude);
+ expect(getVisibleChildren(container)).toEqual('Loading B');
+
+ await resolveB();
+
+ const dynamic = await serverAct(() =>
+ ReactDOMFizzServer.resume(, JSON.parse(postponedState2)),
+ );
+
+ await readIntoContainer(dynamic);
+ expect(getVisibleChildren(container)).toEqual('Hello');
+ });
});
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
index f5d6a45a18..6785515bbe 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
@@ -23,6 +23,7 @@ import ReactVersion from 'shared/ReactVersion';
import {
createPrerenderRequest,
+ resumeAndPrerenderRequest,
startWork,
startFlowing,
stopFlowing,
@@ -33,6 +34,7 @@ import {
import {
createResumableState,
createRenderState,
+ resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -141,4 +143,73 @@ function prerender(
});
}
-export {prerender, ReactVersion as version};
+type ResumeOptions = {
+ nonce?: string,
+ signal?: AbortSignal,
+ onError?: (error: mixed) => ?string,
+ onPostpone?: (reason: string) => void,
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
+};
+
+function resumeAndPrerender(
+ children: ReactNodeList,
+ postponedState: PostponedState,
+ options?: ResumeOptions,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const onFatalError = reject;
+
+ function onAllReady() {
+ const stream = new ReadableStream(
+ {
+ type: 'bytes',
+ pull: (controller): ?Promise => {
+ startFlowing(request, controller);
+ },
+ cancel: (reason): ?Promise => {
+ stopFlowing(request);
+ abort(request, reason);
+ },
+ },
+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
+ {highWaterMark: 0},
+ );
+
+ const result = {
+ postponed: getPostponedState(request),
+ prelude: stream,
+ };
+ resolve(result);
+ }
+
+ const request = resumeAndPrerenderRequest(
+ children,
+ postponedState,
+ resumeRenderState(
+ postponedState.resumableState,
+ options ? options.nonce : undefined,
+ ),
+ options ? options.onError : undefined,
+ onAllReady,
+ undefined,
+ undefined,
+ onFatalError,
+ options ? options.onPostpone : undefined,
+ );
+ if (options && options.signal) {
+ const signal = options.signal;
+ if (signal.aborted) {
+ abort(request, (signal: any).reason);
+ } else {
+ const listener = () => {
+ abort(request, (signal: any).reason);
+ signal.removeEventListener('abort', listener);
+ };
+ signal.addEventListener('abort', listener);
+ }
+ }
+ startWork(request);
+ });
+}
+
+export {prerender, resumeAndPrerender, ReactVersion as version};
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
index 1a2eb1e599..5a7467002c 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
@@ -23,6 +23,7 @@ import ReactVersion from 'shared/ReactVersion';
import {
createPrerenderRequest,
+ resumeAndPrerenderRequest,
startWork,
startFlowing,
stopFlowing,
@@ -33,6 +34,7 @@ import {
import {
createResumableState,
createRenderState,
+ resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -140,4 +142,73 @@ function prerender(
});
}
-export {prerender, ReactVersion as version};
+type ResumeOptions = {
+ nonce?: string,
+ signal?: AbortSignal,
+ onError?: (error: mixed) => ?string,
+ onPostpone?: (reason: string) => void,
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
+};
+
+function resumeAndPrerender(
+ children: ReactNodeList,
+ postponedState: PostponedState,
+ options?: ResumeOptions,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const onFatalError = reject;
+
+ function onAllReady() {
+ const stream = new ReadableStream(
+ {
+ type: 'bytes',
+ pull: (controller): ?Promise => {
+ startFlowing(request, controller);
+ },
+ cancel: (reason): ?Promise => {
+ stopFlowing(request);
+ abort(request, reason);
+ },
+ },
+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
+ {highWaterMark: 0},
+ );
+
+ const result = {
+ postponed: getPostponedState(request),
+ prelude: stream,
+ };
+ resolve(result);
+ }
+
+ const request = resumeAndPrerenderRequest(
+ children,
+ postponedState,
+ resumeRenderState(
+ postponedState.resumableState,
+ options ? options.nonce : undefined,
+ ),
+ options ? options.onError : undefined,
+ onAllReady,
+ undefined,
+ undefined,
+ onFatalError,
+ options ? options.onPostpone : undefined,
+ );
+ if (options && options.signal) {
+ const signal = options.signal;
+ if (signal.aborted) {
+ abort(request, (signal: any).reason);
+ } else {
+ const listener = () => {
+ abort(request, (signal: any).reason);
+ signal.removeEventListener('abort', listener);
+ };
+ signal.addEventListener('abort', listener);
+ }
+ }
+ startWork(request);
+ });
+}
+
+export {prerender, resumeAndPrerender, ReactVersion as version};
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
index fc25aa75c1..9b9cd680c1 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
@@ -25,6 +25,7 @@ import ReactVersion from 'shared/ReactVersion';
import {
createPrerenderRequest,
+ resumeAndPrerenderRequest,
startWork,
startFlowing,
abort,
@@ -34,6 +35,7 @@ import {
import {
createResumableState,
createRenderState,
+ resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -141,4 +143,67 @@ function prerenderToNodeStream(
});
}
-export {prerenderToNodeStream, ReactVersion as version};
+type ResumeOptions = {
+ nonce?: string,
+ signal?: AbortSignal,
+ onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
+ onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
+};
+
+function resumeAndPrerenderToNodeStream(
+ children: ReactNodeList,
+ postponedState: PostponedState,
+ options?: ResumeOptions,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const onFatalError = reject;
+
+ function onAllReady() {
+ const readable: Readable = new Readable({
+ read() {
+ startFlowing(request, writable);
+ },
+ });
+ const writable = createFakeWritable(readable);
+
+ const result = {
+ postponed: getPostponedState(request),
+ prelude: readable,
+ };
+ resolve(result);
+ }
+ const request = resumeAndPrerenderRequest(
+ children,
+ postponedState,
+ resumeRenderState(
+ postponedState.resumableState,
+ options ? options.nonce : undefined,
+ ),
+ options ? options.onError : undefined,
+ onAllReady,
+ undefined,
+ undefined,
+ onFatalError,
+ options ? options.onPostpone : undefined,
+ );
+ if (options && options.signal) {
+ const signal = options.signal;
+ if (signal.aborted) {
+ abort(request, (signal: any).reason);
+ } else {
+ const listener = () => {
+ abort(request, (signal: any).reason);
+ signal.removeEventListener('abort', listener);
+ };
+ signal.addEventListener('abort', listener);
+ }
+ }
+ startWork(request);
+ });
+}
+
+export {
+ prerenderToNodeStream,
+ resumeAndPrerenderToNodeStream,
+ ReactVersion as version,
+};
diff --git a/packages/react-dom/src/server/react-dom-server.browser.js b/packages/react-dom/src/server/react-dom-server.browser.js
index c12bb28c19..5ab1f0e142 100644
--- a/packages/react-dom/src/server/react-dom-server.browser.js
+++ b/packages/react-dom/src/server/react-dom-server.browser.js
@@ -8,4 +8,4 @@
*/
export * from './ReactDOMFizzServerBrowser.js';
-export {prerender} from './ReactDOMFizzStaticBrowser.js';
+export {prerender, resumeAndPrerender} from './ReactDOMFizzStaticBrowser.js';
diff --git a/packages/react-dom/src/server/react-dom-server.edge.js b/packages/react-dom/src/server/react-dom-server.edge.js
index c3882bed01..e70e8fd4cb 100644
--- a/packages/react-dom/src/server/react-dom-server.edge.js
+++ b/packages/react-dom/src/server/react-dom-server.edge.js
@@ -8,4 +8,4 @@
*/
export * from './ReactDOMFizzServerEdge.js';
-export {prerender} from './ReactDOMFizzStaticEdge.js';
+export {prerender, resumeAndPrerender} from './ReactDOMFizzStaticEdge.js';
diff --git a/packages/react-dom/src/server/react-dom-server.node.js b/packages/react-dom/src/server/react-dom-server.node.js
index 9ca72308b0..17c2d755b4 100644
--- a/packages/react-dom/src/server/react-dom-server.node.js
+++ b/packages/react-dom/src/server/react-dom-server.node.js
@@ -8,4 +8,7 @@
*/
export * from './ReactDOMFizzServerNode.js';
-export {prerenderToNodeStream} from './ReactDOMFizzStaticNode.js';
+export {
+ prerenderToNodeStream,
+ resumeAndPrerenderToNodeStream,
+} from './ReactDOMFizzStaticNode.js';
diff --git a/packages/react-dom/static.browser.js b/packages/react-dom/static.browser.js
index f5148e087f..23e07d7dd2 100644
--- a/packages/react-dom/static.browser.js
+++ b/packages/react-dom/static.browser.js
@@ -7,4 +7,8 @@
* @flow
*/
-export {prerender, version} from './src/server/react-dom-server.browser';
+export {
+ prerender,
+ resumeAndPrerender,
+ version,
+} from './src/server/react-dom-server.browser';
diff --git a/packages/react-dom/static.edge.js b/packages/react-dom/static.edge.js
index 40bdffd4e4..e2cbc69286 100644
--- a/packages/react-dom/static.edge.js
+++ b/packages/react-dom/static.edge.js
@@ -7,4 +7,8 @@
* @flow
*/
-export {prerender, version} from './src/server/react-dom-server.edge';
+export {
+ prerender,
+ resumeAndPrerender,
+ version,
+} from './src/server/react-dom-server.edge';
diff --git a/packages/react-dom/static.node.js b/packages/react-dom/static.node.js
index 9036370546..a25c88af4d 100644
--- a/packages/react-dom/static.node.js
+++ b/packages/react-dom/static.node.js
@@ -9,5 +9,6 @@
export {
prerenderToNodeStream,
+ resumeAndPrerenderToNodeStream,
version,
} from './src/server/react-dom-server.node';
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 633aed053b..d0e6eb852c 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -641,6 +641,37 @@ export function resumeRequest(
return request;
}
+export function resumeAndPrerenderRequest(
+ children: ReactNodeList,
+ postponedState: PostponedState,
+ renderState: RenderState,
+ onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),
+ onAllReady: void | (() => void),
+ onShellReady: void | (() => void),
+ onShellError: void | ((error: mixed) => void),
+ onFatalError: void | ((error: mixed) => void),
+ onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
+): Request {
+ const request = resumeRequest(
+ children,
+ postponedState,
+ renderState,
+ onError,
+ onAllReady,
+ onShellReady,
+ onShellError,
+ onFatalError,
+ onPostpone,
+ );
+ // Start tracking postponed holes during this render.
+ request.trackedPostpones = {
+ workingMap: new Map(),
+ rootNodes: [],
+ rootSlots: null,
+ };
+ return request;
+}
+
let currentRequest: null | Request = null;
export function resolveRequest(): null | Request {
@@ -1349,6 +1380,7 @@ function replaySuspenseBoundary(
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
task.blockedBoundary = resumedBoundary;
task.hoistableState = resumedBoundary.contentState;
+ task.keyPath = keyPath;
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
try {
@@ -5066,6 +5098,7 @@ export function abort(request: Request, reason: mixed): void {
if (request.status === OPEN) {
request.status = ABORTING;
}
+
try {
const abortableTasks = request.abortableTasks;
if (abortableTasks.size > 0) {