diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
index bb65ef4b659a7..1434d17015a54 100644
--- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -187,10 +190,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
index ef980764942d7..56c3d5b71f432 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
@@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +149,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
index ef980764942d7..56c3d5b71f432 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
@@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +149,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
index e484d4b7e77d5..f9b0c163b2154 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -189,10 +192,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
index faaf8aef01b0d..a68bcd3d47ff0 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
@@ -2722,4 +2722,142 @@ describe('ReactFlightDOM', () => {
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
hello world
);
});
+
+ // @gate enableHalt
+ it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const controller = new AbortController();
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
+ ,
+ webpackMap,
+ {
+ signal: controller.signal,
+ },
+ ),
+ };
+ });
+
+ controller.abort();
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ const preludeWeb = Readable.toWeb(prelude);
+ const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
+
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
+
+ function ClientApp() {
+ return use(response);
+ }
+
+ const shellErrors = [];
+ let abortFizz;
+ await serverAct(async () => {
+ const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
+ React.createElement(ClientApp),
+ {
+ onShellError(error) {
+ shellErrors.push(error.message);
+ },
+ },
+ );
+ pipe(fizzWritable);
+ abortFizz = abort;
+ });
+
+ await serverAct(() => {
+ try {
+ React.unstable_postpone('abort reason');
+ } catch (reason) {
+ abortFizz(reason);
+ }
+ });
+
+ expect(shellErrors).toEqual([]);
+
+ const container = document.createElement('div');
+ await readInto(container, fizzReadable);
+ expect(getMeaningfulChildren(container)).toEqual(loading...
);
+ });
+
+ // @gate enableHalt
+ it('will leave async iterables in an incomplete state when halting', async () => {
+ let resolve;
+ const wait = new Promise(r => (resolve = r));
+ const errors = [];
+
+ const multiShotIterable = {
+ async *[Symbol.asyncIterator]() {
+ yield {hello: 'A'};
+ await wait;
+ yield {hi: 'B'};
+ return 'C';
+ },
+ };
+
+ const controller = new AbortController();
+ const {pendingResult} = await serverAct(() => {
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
+ {
+ multiShotIterable,
+ },
+ {},
+ {
+ onError(x) {
+ errors.push(x);
+ },
+ signal: controller.signal,
+ },
+ ),
+ };
+ });
+
+ controller.abort();
+ await serverAct(() => resolve());
+
+ const {prelude} = await pendingResult;
+
+ const result = await ReactServerDOMClient.createFromReadableStream(
+ Readable.toWeb(prelude),
+ );
+
+ const iterator = result.multiShotIterable[Symbol.asyncIterator]();
+
+ expect(await iterator.next()).toEqual({
+ value: {hello: 'A'},
+ done: false,
+ });
+
+ const race = Promise.race([
+ iterator.next(),
+ new Promise(r => setTimeout(() => r('timeout'), 10)),
+ ]);
+
+ await 1;
+ jest.advanceTimersByTime('100');
+ expect(await race).toBe('timeout');
+ });
});
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
index db8edf7ad6831..7bbfea1484bed 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
@@ -29,6 +29,7 @@ let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let ReactServerDOMServer;
+let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let Suspense;
let use;
@@ -60,7 +61,13 @@ describe('ReactFlightDOMBrowser', () => {
serverExports = WebpackMock.serverExports;
webpackMap = WebpackMock.webpackMap;
webpackServerMap = WebpackMock.webpackServerMap;
- ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
+ ReactServerDOMServer = require('react-server-dom-webpack/server');
+ if (__EXPERIMENTAL__) {
+ jest.mock('react-server-dom-webpack/static', () =>
+ require('react-server-dom-webpack/static.browser'),
+ );
+ ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
+ }
__unmockReact();
jest.resetModules();
@@ -2332,4 +2339,110 @@ describe('ReactFlightDOMBrowser', () => {
expect(error.digest).toBe('aborted');
expect(errors).toEqual([reason]);
});
+
+ // @gate experimental
+ it('can prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerender(
+ ,
+ webpackMap,
+ ),
+ };
+ });
+
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ function ClientRoot({response}) {
+ return use(response);
+ }
+
+ const response = ReactServerDOMClient.createFromReadableStream(
+ passThrough(prelude),
+ );
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ await act(() => {
+ root.render();
+ });
+ expect(container.innerHTML).toBe('hello world
');
+ });
+
+ // @gate enableHalt
+ it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const controller = new AbortController();
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerender(
+ ,
+ webpackMap,
+ {
+ signal: controller.signal,
+ },
+ ),
+ };
+ });
+
+ controller.abort();
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ function ClientRoot({response}) {
+ return use(response);
+ }
+
+ const response = ReactServerDOMClient.createFromReadableStream(
+ passThrough(prelude),
+ );
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.innerHTML).toBe('loading...
');
+ });
});
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
index ffef621e9761b..0a9af8a9d2689 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
@@ -23,9 +23,9 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
// Patch for Edge environments for global scope
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
-// Don't wait before processing work on the server.
-// TODO: we can replace this with FlightServer.act().
-global.setTimeout = cb => cb();
+const {
+ patchMessageChannel,
+} = require('../../../../scripts/jest/patchMessageChannel');
let serverExports;
let clientExports;
@@ -36,8 +36,12 @@ let React;
let ReactServer;
let ReactDOMServer;
let ReactServerDOMServer;
+let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let use;
+let ReactServerScheduler;
+let reactServerAct;
+let assertConsoleErrorDev;
function normalizeCodeLocInfo(str) {
return (
@@ -52,6 +56,10 @@ describe('ReactFlightDOMEdge', () => {
beforeEach(() => {
jest.resetModules();
+ ReactServerScheduler = require('scheduler');
+ patchMessageChannel(ReactServerScheduler);
+ reactServerAct = require('internal-test-utils').act;
+
// Simulate the condition resolution
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
@@ -68,6 +76,12 @@ describe('ReactFlightDOMEdge', () => {
ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');
+ if (__EXPERIMENTAL__) {
+ jest.mock('react-server-dom-webpack/static', () =>
+ require('react-server-dom-webpack/static.edge'),
+ );
+ ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
+ }
jest.resetModules();
__unmockReact();
@@ -79,8 +93,22 @@ describe('ReactFlightDOMEdge', () => {
ReactDOMServer = require('react-dom/server.edge');
ReactServerDOMClient = require('react-server-dom-webpack/client');
use = React.use;
+
+ assertConsoleErrorDev =
+ require('internal-test-utils').assertConsoleErrorDev;
});
+ async function serverAct(callback) {
+ let maybePromise;
+ await reactServerAct(() => {
+ maybePromise = callback();
+ if (maybePromise && typeof maybePromise.catch === 'function') {
+ maybePromise.catch(() => {});
+ }
+ });
+ return maybePromise;
+ }
+
function passThrough(stream) {
// Simulate more realistic network by splitting up and rejoining some chunks.
// This lets us test that we don't accidentally rely on particular bounds of the chunks.
@@ -174,9 +202,8 @@ describe('ReactFlightDOMEdge', () => {
return ;
}
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- webpackMap,
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(, webpackMap),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
@@ -189,8 +216,8 @@ describe('ReactFlightDOMEdge', () => {
return use(response);
}
- const ssrStream = await ReactDOMServer.renderToReadableStream(
- ,
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(),
);
const result = await readResult(ssrStream);
expect(result).toEqual('Client Component');
@@ -200,10 +227,12 @@ describe('ReactFlightDOMEdge', () => {
const testString = '"\n\t'.repeat(500) + '🙃';
const testString2 = 'hello'.repeat(400);
- const stream = ReactServerDOMServer.renderToReadableStream({
- text: testString,
- text2: testString2,
- });
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream({
+ text: testString,
+ text2: testString2,
+ }),
+ );
const [stream1, stream2] = passThrough(stream).tee();
const serializedContent = await readResult(stream1);
@@ -234,7 +263,9 @@ describe('ReactFlightDOMEdge', () => {
with: {many: 'properties in it'},
};
const props = {root: {new Array(30).fill(obj)}
};
- const stream = ReactServerDOMServer.renderToReadableStream(props);
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(props),
+ );
const [stream1, stream2] = passThrough(stream).tee();
const serializedContent = await readResult(stream1);
@@ -302,7 +333,9 @@ describe('ReactFlightDOMEdge', () => {
>
);
const resolvedChildren = new Array(30).fill(str);
- const stream = ReactServerDOMServer.renderToReadableStream(children);
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(children),
+ );
const [stream1, stream2] = passThrough(stream).tee();
const serializedContent = await readResult(stream1);
@@ -318,7 +351,9 @@ describe('ReactFlightDOMEdge', () => {
});
// Use the SSR render to resolve any lazy elements
- const ssrStream = await ReactDOMServer.renderToReadableStream(model);
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(model),
+ );
// Should still match the result when parsed
const result = await readResult(ssrStream);
expect(result).toEqual(resolvedChildren.join(''));
@@ -370,22 +405,28 @@ describe('ReactFlightDOMEdge', () => {
const resolvedChildren = new Array(30).fill(
'this is a long return value
',
);
- const stream = ReactServerDOMServer.renderToReadableStream(children);
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(children),
+ );
const [stream1, stream2] = passThrough(stream).tee();
const serializedContent = await readResult(stream1);
expect(serializedContent.length).toBeLessThan(__DEV__ ? 605 : 400);
expect(timesRendered).toBeLessThan(5);
- const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
- ssrManifest: {
- moduleMap: null,
- moduleLoading: null,
- },
- });
+ const model = await serverAct(() =>
+ ReactServerDOMClient.createFromReadableStream(stream2, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ }),
+ );
// Use the SSR render to resolve any lazy elements
- const ssrStream = await ReactDOMServer.renderToReadableStream(model);
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(model),
+ );
// Should still match the result when parsed
const result = await readResult(ssrStream);
expect(result).toEqual(resolvedChildren.join(''));
@@ -398,8 +439,10 @@ describe('ReactFlightDOMEdge', () => {
}
return Fin
;
}
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(
+ ,
+ ),
);
const serializedContent = await readResult(stream);
const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0;
@@ -426,8 +469,8 @@ describe('ReactFlightDOMEdge', () => {
new BigUint64Array(buffer, 0),
new DataView(buffer, 3),
];
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(buffers),
+ const stream = await serverAct(() =>
+ passThrough(ReactServerDOMServer.renderToReadableStream(buffers)),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
@@ -446,8 +489,8 @@ describe('ReactFlightDOMEdge', () => {
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(blob),
+ const stream = await serverAct(() =>
+ passThrough(ReactServerDOMServer.renderToReadableStream(blob)),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
@@ -476,8 +519,8 @@ describe('ReactFlightDOMEdge', () => {
expect(formData.get('file') instanceof File).toBe(true);
expect(formData.get('file').name).toBe('filename.test');
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(formData),
+ const stream = await serverAct(() =>
+ passThrough(ReactServerDOMServer.renderToReadableStream(formData)),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
@@ -507,8 +550,8 @@ describe('ReactFlightDOMEdge', () => {
const map = new Map();
map.set('value', awaitedValue);
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(map, webpackMap),
+ const stream = await serverAct(() =>
+ passThrough(ReactServerDOMServer.renderToReadableStream(map, webpackMap)),
);
// Parsing the root blocks because the module hasn't loaded yet
@@ -549,16 +592,18 @@ describe('ReactFlightDOMEdge', () => {
},
});
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(s, webpackMap),
+ const stream = await serverAct(() =>
+ passThrough(ReactServerDOMServer.renderToReadableStream(s, webpackMap)),
);
- const result = await ReactServerDOMClient.createFromReadableStream(stream, {
- ssrManifest: {
- moduleMap: null,
- moduleLoading: null,
- },
- });
+ const result = await serverAct(() =>
+ ReactServerDOMClient.createFromReadableStream(stream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ }),
+ );
const reader = result.getReader();
@@ -589,20 +634,24 @@ describe('ReactFlightDOMEdge', () => {
},
};
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(
- multiShotIterable,
- webpackMap,
+ const stream = await serverAct(() =>
+ passThrough(
+ ReactServerDOMServer.renderToReadableStream(
+ multiShotIterable,
+ webpackMap,
+ ),
),
);
// Parsing the root blocks because the module hasn't loaded yet
- const result = await ReactServerDOMClient.createFromReadableStream(stream, {
- ssrManifest: {
- moduleMap: null,
- moduleLoading: null,
- },
- });
+ const result = await serverAct(() =>
+ ReactServerDOMClient.createFromReadableStream(stream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ }),
+ );
const iterator = result[Symbol.asyncIterator]();
@@ -635,9 +684,11 @@ describe('ReactFlightDOMEdge', () => {
},
};
- const stream = ReactServerDOMServer.renderToReadableStream({
- iterable,
- });
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream({
+ iterable,
+ }),
+ );
const [stream1, stream2] = passThrough(stream).tee();
const serializedContent = await readResult(stream1);
@@ -728,7 +779,9 @@ describe('ReactFlightDOMEdge', () => {
},
});
- const stream = ReactServerDOMServer.renderToReadableStream(s, {});
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(s, {}),
+ );
const [stream1, stream2] = passThrough(stream).tee();
@@ -785,7 +838,9 @@ describe('ReactFlightDOMEdge', () => {
},
});
- const stream = ReactServerDOMServer.renderToReadableStream(s, {});
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(s, {}),
+ );
const [stream1, stream2] = passThrough(stream).tee();
@@ -841,23 +896,21 @@ describe('ReactFlightDOMEdge', () => {
greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
};
- const stream = ReactServerDOMServer.renderToReadableStream(
- model,
- webpackMap,
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(model, webpackMap),
);
- const rootModel = await ReactServerDOMClient.createFromReadableStream(
- stream,
- {
+ const rootModel = await serverAct(() =>
+ ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
- },
+ }),
);
- const ssrStream = await ReactDOMServer.renderToReadableStream(
- rootModel.greeting,
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(rootModel.greeting),
);
const result = await readResult(ssrStream);
expect(result).toEqual('Hello, Seb');
@@ -916,13 +969,15 @@ describe('ReactFlightDOMEdge', () => {
return ReactServer.createElement('span', null, 'hi');
}
- const stream = ReactServerDOMServer.renderToReadableStream(
- ReactServer.createElement(
- 'div',
- null,
- ReactServer.createElement(Foo, null),
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(
+ ReactServer.createElement(
+ 'div',
+ null,
+ ReactServer.createElement(Foo, null),
+ ),
+ webpackMap,
),
- webpackMap,
);
await readResult(stream);
@@ -943,35 +998,31 @@ describe('ReactFlightDOMEdge', () => {
root: ReactServer.createElement(Erroring),
};
- const stream = ReactServerDOMServer.renderToReadableStream(
- model,
- webpackMap,
- {
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
onError() {},
- },
+ }),
);
- const rootModel = await ReactServerDOMClient.createFromReadableStream(
- stream,
- {
+ const rootModel = await serverAct(() =>
+ ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
- },
+ }),
);
const errors = [];
- const result = ReactDOMServer.renderToReadableStream(
- {rootModel.root}
,
- {
+ const result = serverAct(() =>
+ ReactDOMServer.renderToReadableStream({rootModel.root}
, {
onError(error, {componentStack}) {
errors.push({
error,
componentStack: normalizeCodeLocInfo(componentStack),
});
},
- },
+ }),
);
const theError = new Error('my error');
@@ -1000,4 +1051,127 @@ describe('ReactFlightDOMEdge', () => {
},
]);
});
+
+ // @gate experimental
+ it('can prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerender(
+ ,
+ webpackMap,
+ ),
+ };
+ });
+
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ function ClientRoot({response}) {
+ return use(response);
+ }
+
+ const response = ReactServerDOMClient.createFromReadableStream(prelude, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ // Use the SSR render to resolve any lazy elements
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(
+ React.createElement(ClientRoot, {response}),
+ ),
+ );
+ // Should still match the result when parsed
+ const result = await readResult(ssrStream);
+ expect(result).toBe('hello world
');
+ });
+
+ // @gate enableHalt
+ it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const controller = new AbortController();
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerender(
+ ,
+ webpackMap,
+ {
+ signal: controller.signal,
+ },
+ ),
+ };
+ });
+
+ controller.abort();
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ function ClientRoot({response}) {
+ return use(response);
+ }
+
+ const response = ReactServerDOMClient.createFromReadableStream(prelude, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ const fizzController = new AbortController();
+ // Use the SSR render to resolve any lazy elements
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(
+ React.createElement(ClientRoot, {response}),
+ {
+ signal: fizzController.signal,
+ },
+ ),
+ );
+ fizzController.abort('boom');
+ assertConsoleErrorDev(['boom'], {withoutStack: true});
+ // Should still match the result when parsed
+ const result = await readResult(ssrStream);
+ const div = document.createElement('div');
+ div.innerHTML = result;
+ expect(div.textContent).toBe('loading...');
+ });
});
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
index 2de34cc1c493f..3930663e47e46 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
@@ -20,12 +20,15 @@ let webpackModules;
let webpackModuleLoading;
let React;
let ReactDOMServer;
+let ReactServer;
let ReactServerDOMServer;
+let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let Stream;
let use;
let ReactServerScheduler;
let reactServerAct;
+let assertConsoleErrorDev;
// We test pass-through without encoding strings but it should work without it too.
const streamOptions = {
@@ -45,7 +48,14 @@ describe('ReactFlightDOMNode', () => {
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.node'),
);
+ ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');
+ if (__EXPERIMENTAL__) {
+ jest.mock('react-server-dom-webpack/static', () =>
+ require('react-server-dom-webpack/static.node'),
+ );
+ ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
+ }
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
@@ -65,6 +75,9 @@ describe('ReactFlightDOMNode', () => {
ReactServerDOMClient = require('react-server-dom-webpack/client');
Stream = require('stream');
use = React.use;
+
+ assertConsoleErrorDev =
+ require('internal-test-utils').assertConsoleErrorDev;
});
async function serverAct(callback) {
@@ -378,4 +391,122 @@ describe('ReactFlightDOMNode', () => {
expect(error.digest).toBe('aborted');
expect(errors).toEqual([reason]);
});
+
+ // @gate experimental
+ it('can prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
+ ,
+ webpackMap,
+ ),
+ };
+ });
+
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ function ClientRoot({response}) {
+ return use(response);
+ }
+
+ const response = ReactServerDOMClient.createFromNodeStream(prelude, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ // Use the SSR render to resolve any lazy elements
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToPipeableStream(
+ React.createElement(ClientRoot, {response}),
+ ),
+ );
+ // Should still match the result when parsed
+ const result = await readResult(ssrStream);
+ expect(result).toBe('hello world
');
+ });
+
+ // @gate enableHalt
+ it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const controller = new AbortController();
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
+ ,
+ webpackMap,
+ {
+ signal: controller.signal,
+ },
+ ),
+ };
+ });
+
+ controller.abort();
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ function ClientRoot({response}) {
+ return use(response);
+ }
+
+ const response = ReactServerDOMClient.createFromNodeStream(prelude, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToPipeableStream(
+ React.createElement(ClientRoot, {response}),
+ ),
+ );
+ ssrStream.abort('boom');
+ assertConsoleErrorDev(['boom'], {withoutStack: true});
+ // Should still match the result when parsed
+ const result = await readResult(ssrStream);
+ const div = document.createElement('div');
+ div.innerHTML = result;
+ expect(div.textContent).toBe('loading...');
+ });
});
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
index a4e0c3bef693b..95e7f770428a3 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
@@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +149,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
index a4e0c3bef693b..95e7f770428a3 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
@@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +149,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
index 1506259476703..1d8d6ea9ef743 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream';
+import {enableHalt} from 'shared/ReactFeatureFlags';
+
import {
createRequest,
startWork,
startFlowing,
stopFlowing,
abort,
+ halt,
} from 'react-server/src/ReactFlightServer';
import {
@@ -189,10 +192,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (enableHalt) {
+ halt(request, reason);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index d8ba106d37e72..b09fe88a886c3 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -16,6 +16,7 @@ import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences
import {
enableBinaryFlight,
enablePostpone,
+ enableHalt,
enableTaint,
enableRefAsProp,
enableServerComponentLogs,
@@ -748,23 +749,38 @@ function serializeReadableStream(
}
aborted = true;
request.abortListeners.delete(error);
+
+ let cancelWith: mixed;
if (
+ enableHalt &&
+ typeof reason === 'object' &&
+ reason !== null &&
+ (reason: any).$$typeof === haltSymbol
+ ) {
+ const haltInstance: Halt = (reason: any);
+ cancelWith = haltInstance.reason;
+ } else if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
+ cancelWith = reason;
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, streamTask);
emitPostponeChunk(request, streamTask.id, postponeInstance);
+ enqueueFlush(request);
} else {
+ cancelWith = reason;
const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason);
+ enqueueFlush(request);
}
- enqueueFlush(request);
+
// $FlowFixMe should be able to pass mixed
- reader.cancel(reason).then(error, error);
+ reader.cancel(cancelWith).then(error, error);
}
+
request.abortListeners.add(error);
reader.read().then(progress, error);
return serializeByValueID(streamTask.id);
@@ -866,24 +882,36 @@ function serializeAsyncIterable(
}
aborted = true;
request.abortListeners.delete(error);
+ let throwWith: mixed;
if (
+ enableHalt &&
+ typeof reason === 'object' &&
+ reason !== null &&
+ (reason: any).$$typeof === haltSymbol
+ ) {
+ const haltInstance: Halt = (reason: any);
+ throwWith = haltInstance.reason;
+ } else if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
+ throwWith = reason;
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, streamTask);
emitPostponeChunk(request, streamTask.id, postponeInstance);
+ enqueueFlush(request);
} else {
+ throwWith = reason;
const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason);
+ enqueueFlush(request);
}
- enqueueFlush(request);
if (typeof (iterator: any).throw === 'function') {
// The iterator protocol doesn't necessarily include this but a generator do.
// $FlowFixMe should be able to pass mixed
- iterator.throw(reason).then(error, error);
+ iterator.throw(throwWith).then(error, error);
}
}
request.abortListeners.add(error);
@@ -2066,12 +2094,24 @@ function serializeBlob(request: Request, blob: Blob): string {
}
aborted = true;
request.abortListeners.delete(error);
- const digest = logRecoverableError(request, reason, newTask);
- emitErrorChunk(request, newTask.id, digest, reason);
- request.abortableTasks.delete(newTask);
- enqueueFlush(request);
+ let cancelWith: mixed;
+ if (
+ enableHalt &&
+ typeof reason === 'object' &&
+ reason !== null &&
+ (reason: any).$$typeof === haltSymbol
+ ) {
+ const haltInstance: Halt = (reason: any);
+ cancelWith = haltInstance.reason;
+ } else {
+ cancelWith = reason;
+ const digest = logRecoverableError(request, reason, newTask);
+ emitErrorChunk(request, newTask.id, digest, reason);
+ request.abortableTasks.delete(newTask);
+ enqueueFlush(request);
+ }
// $FlowFixMe should be able to pass mixed
- reader.cancel(reason).then(error, error);
+ reader.cancel(cancelWith).then(error, error);
}
request.abortListeners.add(error);
@@ -4012,3 +4052,46 @@ export function abort(request: Request, reason: mixed): void {
fatalError(request, error);
}
}
+
+const haltSymbol = Symbol('halt');
+type Halt = {
+ $$typeof: symbol,
+ reason: mixed,
+};
+
+// This is called to stop rendering without erroring. All unfinished work is represented Promises
+// that never resolve.
+export function halt(request: Request, reason: mixed): void {
+ try {
+ if (request.status === OPEN) {
+ request.status = ABORTING;
+ }
+ const haltInstance: Halt = {
+ $$typeof: haltSymbol,
+ reason,
+ };
+ const abortableTasks = request.abortableTasks;
+ // We have tasks to abort. We'll emit one error row and then emit a reference
+ // to that row from every row that's still remaining.
+ if (abortableTasks.size > 0) {
+ request.pendingChunks++;
+ const refId = request.nextChunkId++;
+ request.fatalError = refId;
+ const model = stringify(serializeInfinitePromise());
+ emitModelChunk(request, refId, model);
+ abortableTasks.forEach(task => abortTask(task, request, refId));
+ abortableTasks.clear();
+ }
+ const abortListeners = request.abortListeners;
+ if (abortListeners.size > 0) {
+ abortListeners.forEach(callback => callback(haltInstance));
+ abortListeners.clear();
+ }
+ if (request.destination !== null) {
+ flushCompletedChunks(request, request.destination);
+ }
+ } catch (error) {
+ logRecoverableError(request, error, null);
+ fatalError(request, error);
+ }
+}
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index b0286405a6cca..c5351d6d92631 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -87,6 +87,8 @@ export const enableTaint = __EXPERIMENTAL__;
export const enablePostpone = __EXPERIMENTAL__;
+export const enableHalt = __EXPERIMENTAL__;
+
/**
* Switches the Fabric API from doing layout in commit work instead of complete work.
*/
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 4eda27d16cfcb..3618aa70e7d67 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -58,6 +58,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
export const enableFizzExternalRuntime = true;
export const enableFlightReadableStream = true;
export const enableGetInspectorDataForInstanceInProduction = true;
+export const enableHalt = false;
export const enableInfiniteRenderLoopDetection = true;
export const enableContextProfiling = false;
export const enableLegacyCache = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 2a4421f41da0a..2aae8bd3d1c65 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -49,6 +49,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
export const enableFizzExternalRuntime = true;
export const enableFlightReadableStream = true;
export const enableGetInspectorDataForInstanceInProduction = false;
+export const enableHalt = false;
export const enableInfiniteRenderLoopDetection = true;
export const enableLazyContextPropagation = false;
export const enableContextProfiling = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 8778bf6558cb4..c44e7014fc444 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -25,6 +25,7 @@ export const enableFlightReadableStream = true;
export const enableAsyncIterableChildren = false;
export const enableTaint = true;
export const enablePostpone = false;
+export const enableHalt = false;
export const disableCommentsAsDOMContainers = true;
export const disableInputAttributeSyncing = false;
export const disableIEWorkarounds = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index 3a8a0c1d44cec..bc7ddf85acc03 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -40,6 +40,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
export const enableFizzExternalRuntime = true;
export const enableFlightReadableStream = true;
export const enableGetInspectorDataForInstanceInProduction = false;
+export const enableHalt = false;
export const enableInfiniteRenderLoopDetection = true;
export const enableLazyContextPropagation = false;
export const enableContextProfiling = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index eb801d7bac4b6..57f60c24aef45 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -25,6 +25,7 @@ export const enableFlightReadableStream = true;
export const enableAsyncIterableChildren = false;
export const enableTaint = true;
export const enablePostpone = false;
+export const enableHalt = false;
export const disableCommentsAsDOMContainers = true;
export const disableInputAttributeSyncing = false;
export const disableIEWorkarounds = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 95cd1e5a6ebe6..465fa58590bcc 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -78,6 +78,8 @@ export const enableTaint = false;
export const enablePostpone = false;
+export const enableHalt = false;
+
export const enableContextProfiling = true;
// TODO: www currently relies on this feature. It's disabled in open source.