diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index cca05ad887242..703e3280da58b 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -1106,7 +1106,13 @@ describe('ReactDOMFizzServer', () => {
expect(Scheduler).toFlushAndYield([]);
expectErrors(
errors,
- [['This Suspense boundary was aborted by the server.', expectedDigest]],
+ [
+ [
+ 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
+ expectedDigest,
+ componentStack(['h1', 'Suspense', 'div', 'App']),
+ ],
+ ],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -3057,6 +3063,178 @@ describe('ReactDOMFizzServer', () => {
);
});
+ // @gate experimental
+ it('Supports custom abort reasons with a string', async () => {
+ function App() {
+ return (
+
',
+ {
+ runScripts: 'dangerously',
+ },
+ );
+ document = jsdom.window.document;
+ container = document.getElementById('container');
+ });
+
+ // @gate experimental
+ it('refers users to apis that support Suspense when somethign suspends', () => {
+ function App({isClient}) {
+ return (
+
+
+ {isClient ? 'resolved' : }
+
+
+ );
+ }
+ container.innerHTML = ReactDOMFizzServer.renderToString(
+
,
+ );
- // @gate experimental
- it('should stream large contents that might overlow individual buffers', async () => {
- const str492 = `(492) This string is intentionally 492 bytes long because we want to make sure we process chunks that will overflow buffer boundaries. It will repeat to fill out the bytes required (inclusive of this prompt):: foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux q :: total count (492)`;
- const str2049 = `(2049) This string is intentionally 2049 bytes long because we want to make sure we process chunks that will overflow buffer boundaries. It will repeat to fill out the bytes required (inclusive of this prompt):: foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy :: total count (2049)`;
-
- // this specific layout is somewhat contrived to exercise the landing on
- // an exact view boundary. it's not critical to test this edge case but
- // since we are setting up a test in general for larger chunks I contrived it
- // as such for now. I don't think it needs to be maintained if in the future
- // the view sizes change or become dynamic becasue of the use of byobRequest
- let stream;
- stream = await ReactDOMFizzServer.renderToReadableStream(
- <>
-
- {''}
-
-
{str492}
-
{str492}
- >,
- );
-
- let result;
- result = await readResult(stream);
- expect(result).toMatchInlineSnapshot(
- `"
${str492}
${str492}
"`,
- );
-
- // this size 2049 was chosen to be a couple base 2 orders larger than the current view
- // size. if the size changes in the future hopefully this will still exercise
- // a chunk that is too large for the view size.
- stream = await ReactDOMFizzServer.renderToReadableStream(
- <>
-
{str2049}
- >,
- );
-
- result = await readResult(stream);
- expect(result).toMatchInlineSnapshot(`"
${str2049}
"`);
+ const errors = [];
+ ReactDOMClient.hydrateRoot(container,
, {
+ onRecoverableError(error, errorInfo) {
+ errors.push(error.message);
+ },
+ });
+
+ expect(Scheduler).toFlushAndYield([]);
+ expect(errors.length).toBe(1);
+ if (__DEV__) {
+ expect(errors[0]).toBe(
+ 'The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server',
+ );
+ } else {
+ expect(errors[0]).toBe(
+ 'The server could not finish this Suspense boundary, likely due to ' +
+ 'an error during server rendering. Switched to client rendering.',
+ );
+ }
+ });
});
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
index a625a8df0e2f0..303275bab5ecc 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
@@ -226,7 +226,7 @@ describe('ReactDOMFizzServer', () => {
expect(output.result).toBe('');
expect(reportedErrors).toEqual([
theError.message,
- 'This Suspense boundary was aborted by the server.',
+ 'The destination stream errored while writing data.',
]);
expect(reportedShellErrors).toEqual([theError]);
});
@@ -317,13 +317,11 @@ describe('ReactDOMFizzServer', () => {
expect(output.result).toContain('Loading');
expect(isCompleteCalls).toBe(0);
- abort();
+ abort(new Error('uh oh'));
await completed;
- expect(errors).toEqual([
- 'This Suspense boundary was aborted by the server.',
- ]);
+ expect(errors).toEqual(['uh oh']);
expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
expect(isCompleteCalls).toBe(1);
@@ -365,8 +363,8 @@ describe('ReactDOMFizzServer', () => {
expect(errors).toEqual([
// There are two boundaries that abort
- 'This Suspense boundary was aborted by the server.',
- 'This Suspense boundary was aborted by the server.',
+ 'The render was aborted by the server without a reason.',
+ 'The render was aborted by the server without a reason.',
]);
expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
@@ -603,7 +601,7 @@ describe('ReactDOMFizzServer', () => {
await completed;
expect(errors).toEqual([
- 'This Suspense boundary was aborted by the server.',
+ 'The destination stream errored while writing data.',
]);
expect(rendered).toBe(false);
expect(isComplete).toBe(true);
diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
index f2ddab69cdd0e..e8e7dffee5d6d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
@@ -830,7 +830,7 @@ describe('ReactDOMServerHydration', () => {
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
Array [
- "Caught [This Suspense boundary was aborted by the server.]",
+ "Caught [The server did not finish this Suspense boundary: The server used \\"renderToString\\" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to \\"renderToPipeableStream\\" which supports Suspense on the server]",
]
`);
}
@@ -865,7 +865,7 @@ describe('ReactDOMServerHydration', () => {
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
Array [
- "Caught [This Suspense boundary was aborted by the server.]",
+ "Caught [The server did not finish this Suspense boundary: The server used \\"renderToString\\" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to \\"renderToPipeableStream\\" which supports Suspense on the server]",
]
`);
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index c27eabe3e97dc..e086448d6914d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -1674,11 +1674,17 @@ describe('ReactDOMServerPartialHydration', () => {
// we exclude fb bundles with partial renderer
if (__DEV__ && !usingPartialRenderer) {
expect(Scheduler).toFlushAndYield([
- 'This Suspense boundary was aborted by the server.',
+ 'The server did not finish this Suspense boundary: The server used' +
+ ' "renderToString" which does not support Suspense. If you intended' +
+ ' for this Suspense boundary to render the fallback content on the' +
+ ' server consider throwing an Error somewhere within the Suspense boundary.' +
+ ' If you intended to have the server wait for the suspended component' +
+ ' please switch to "renderToPipeableStream" which supports Suspense on the server',
]);
} else {
expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ 'The server could not finish this Suspense boundary, likely due to ' +
+ 'an error during server rendering. Switched to client rendering.',
]);
}
jest.runAllTimers();
@@ -1742,11 +1748,17 @@ describe('ReactDOMServerPartialHydration', () => {
// we exclude fb bundles with partial renderer
if (__DEV__ && !usingPartialRenderer) {
expect(Scheduler).toFlushAndYield([
- 'This Suspense boundary was aborted by the server.',
+ 'The server did not finish this Suspense boundary: The server used' +
+ ' "renderToString" which does not support Suspense. If you intended' +
+ ' for this Suspense boundary to render the fallback content on the' +
+ ' server consider throwing an Error somewhere within the Suspense boundary.' +
+ ' If you intended to have the server wait for the suspended component' +
+ ' please switch to "renderToPipeableStream" which supports Suspense on the server',
]);
} else {
expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ 'The server could not finish this Suspense boundary, likely due to ' +
+ 'an error during server rendering. Switched to client rendering.',
]);
}
// This will have exceeded the suspended time so we should timeout.
@@ -1815,11 +1827,17 @@ describe('ReactDOMServerPartialHydration', () => {
// we exclude fb bundles with partial renderer
if (__DEV__ && !usingPartialRenderer) {
expect(Scheduler).toFlushAndYield([
- 'This Suspense boundary was aborted by the server.',
+ 'The server did not finish this Suspense boundary: The server used' +
+ ' "renderToString" which does not support Suspense. If you intended' +
+ ' for this Suspense boundary to render the fallback content on the' +
+ ' server consider throwing an Error somewhere within the Suspense boundary.' +
+ ' If you intended to have the server wait for the suspended component' +
+ ' please switch to "renderToPipeableStream" which supports Suspense on the server',
]);
} else {
expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ 'The server could not finish this Suspense boundary, likely due to ' +
+ 'an error during server rendering. Switched to client rendering.',
]);
}
// This will have exceeded the suspended time so we should timeout.
@@ -2139,11 +2157,17 @@ describe('ReactDOMServerPartialHydration', () => {
// we exclude fb bundles with partial renderer
if (__DEV__ && !usingPartialRenderer) {
expect(Scheduler).toFlushAndYield([
- 'This Suspense boundary was aborted by the server.',
+ 'The server did not finish this Suspense boundary: The server used' +
+ ' "renderToString" which does not support Suspense. If you intended' +
+ ' for this Suspense boundary to render the fallback content on the' +
+ ' server consider throwing an Error somewhere within the Suspense boundary.' +
+ ' If you intended to have the server wait for the suspended component' +
+ ' please switch to "renderToPipeableStream" which supports Suspense on the server',
]);
} else {
expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ 'The server could not finish this Suspense boundary, likely due to ' +
+ 'an error during server rendering. Switched to client rendering.',
]);
}
@@ -2208,11 +2232,17 @@ describe('ReactDOMServerPartialHydration', () => {
// we exclude fb bundles with partial renderer
if (__DEV__ && !usingPartialRenderer) {
expect(Scheduler).toFlushAndYield([
- 'This Suspense boundary was aborted by the server.',
+ 'The server did not finish this Suspense boundary: The server used' +
+ ' "renderToString" which does not support Suspense. If you intended' +
+ ' for this Suspense boundary to render the fallback content on the' +
+ ' server consider throwing an Error somewhere within the Suspense boundary.' +
+ ' If you intended to have the server wait for the suspended component' +
+ ' please switch to "renderToPipeableStream" which supports Suspense on the server',
]);
} else {
expect(Scheduler).toFlushAndYield([
- 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ 'The server could not finish this Suspense boundary, likely due to ' +
+ 'an error during server rendering. Switched to client rendering.',
]);
}
jest.runAllTimers();
diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
index 23be43a7735a5..fed6988077187 100644
--- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
@@ -14,6 +14,7 @@ let React;
let ReactDOMServer;
let PropTypes;
let ReactCurrentDispatcher;
+let useingPartialRenderer;
describe('ReactDOMServer', () => {
beforeEach(() => {
@@ -24,6 +25,8 @@ describe('ReactDOMServer', () => {
ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;
+
+ useingPartialRenderer = global.__WWW__ && !__EXPERIMENTAL__;
});
describe('renderToString', () => {
@@ -562,6 +565,23 @@ describe('ReactDOMServer', () => {
'Bad lazy',
);
});
+
+ it('aborts synchronously any suspended tasks and renders their fallbacks', () => {
+ const promise = new Promise(res => {});
+ function Suspender() {
+ throw promise;
+ }
+ const response = ReactDOMServer.renderToStaticMarkup(
+
+
+ ,
+ );
+ if (useingPartialRenderer) {
+ expect(response).toEqual('fallback');
+ } else {
+ expect(response).toEqual('fallback');
+ }
+ });
});
describe('renderToNodeStream', () => {
@@ -618,6 +638,41 @@ describe('ReactDOMServer', () => {
expect(response.read()).toBeNull();
});
});
+
+ it('should refer users to new apis when using suspense', async () => {
+ let resolve = null;
+ const promise = new Promise(res => {
+ resolve = () => {
+ resolved = true;
+ res();
+ };
+ });
+ let resolved = false;
+ function Suspender() {
+ if (resolved) {
+ return 'resolved';
+ }
+ throw promise;
+ }
+
+ let response;
+ expect(() => {
+ response = ReactDOMServer.renderToNodeStream(
+
+
+
+
+
,
+ );
+ }).toErrorDev(
+ 'renderToNodeStream is deprecated. Use renderToPipeableStream instead.',
+ {withoutStack: true},
+ );
+ await resolve();
+ expect(response.read().toString()).toEqual(
+ '
resolved
',
+ );
+ });
});
it('warns with a no-op when an async setState is triggered', () => {
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
index 35fbf0e6023c0..fc35fdb286798 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
@@ -97,7 +97,7 @@ function renderToReadableStream(
if (options && options.signal) {
const signal = options.signal;
const listener = () => {
- abort(request);
+ abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
index c5318c2024aa2..ce2b2e5030869 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
@@ -28,8 +28,8 @@ function createDrainHandler(destination, request) {
return () => startFlowing(request, destination);
}
-function createAbortHandler(request) {
- return () => abort(request);
+function createAbortHandler(request, reason) {
+ return () => abort(request, reason);
}
type Options = {|
@@ -90,11 +90,26 @@ function renderToPipeableStream(
hasStartedFlowing = true;
startFlowing(request, destination);
destination.on('drain', createDrainHandler(destination, request));
- destination.on('close', createAbortHandler(request));
+ destination.on(
+ 'error',
+ createAbortHandler(
+ request,
+ // eslint-disable-next-line react-internal/prod-error-codes
+ new Error('The destination stream errored while writing data.'),
+ ),
+ );
+ destination.on(
+ 'close',
+ createAbortHandler(
+ request,
+ // eslint-disable-next-line react-internal/prod-error-codes
+ new Error('The destination stream closed early.'),
+ ),
+ );
return destination;
},
- abort() {
- abort(request);
+ abort(reason) {
+ abort(request, reason);
},
};
}
diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js b/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js
index 168f38fc59db3..71786e4b5078f 100644
--- a/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js
@@ -7,104 +7,36 @@
* @flow
*/
-import ReactVersion from 'shared/ReactVersion';
-
import type {ReactNodeList} from 'shared/ReactTypes';
-import {
- createRequest,
- startWork,
- startFlowing,
- abort,
-} from 'react-server/src/ReactFizzServer';
-
-import {
- createResponseState,
- createRootFormatContext,
-} from './ReactDOMServerLegacyFormatConfig';
+import {version, renderToStringImpl} from './ReactDOMLegacyServerImpl';
type ServerOptions = {
identifierPrefix?: string,
};
-function onError() {
- // Non-fatal errors are ignored.
-}
-
-function renderToStringImpl(
- children: ReactNodeList,
- options: void | ServerOptions,
- generateStaticMarkup: boolean,
-): string {
- let didFatal = false;
- let fatalError = null;
- let result = '';
- const destination = {
- push(chunk) {
- if (chunk !== null) {
- result += chunk;
- }
- return true;
- },
- destroy(error) {
- didFatal = true;
- fatalError = error;
- },
- };
-
- let readyToStream = false;
- function onShellReady() {
- readyToStream = true;
- }
- const request = createRequest(
- children,
- createResponseState(
- generateStaticMarkup,
- options ? options.identifierPrefix : undefined,
- ),
- createRootFormatContext(),
- Infinity,
- onError,
- undefined,
- onShellReady,
- undefined,
- undefined,
- );
- startWork(request);
- // If anything suspended and is still pending, we'll abort it before writing.
- // That way we write only client-rendered boundaries from the start.
- abort(request);
- startFlowing(request, destination);
- if (didFatal) {
- throw fatalError;
- }
-
- if (!readyToStream) {
- // Note: This error message is the one we use on the client. It doesn't
- // really make sense here. But this is the legacy server renderer, anyway.
- // We're going to delete it soon.
- throw new Error(
- 'A component suspended while responding to synchronous input. This ' +
- 'will cause the UI to be replaced with a loading indicator. To fix, ' +
- 'updates that suspend should be wrapped with startTransition.',
- );
- }
-
- return result;
-}
-
function renderToString(
children: ReactNodeList,
options?: ServerOptions,
): string {
- return renderToStringImpl(children, options, false);
+ return renderToStringImpl(
+ children,
+ options,
+ false,
+ 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server',
+ );
}
function renderToStaticMarkup(
children: ReactNodeList,
options?: ServerOptions,
): string {
- return renderToStringImpl(children, options, true);
+ return renderToStringImpl(
+ children,
+ options,
+ true,
+ 'The server used "renderToStaticMarkup" which does not support Suspense. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server',
+ );
}
function renderToNodeStream() {
@@ -126,5 +58,5 @@ export {
renderToStaticMarkup,
renderToNodeStream,
renderToStaticNodeStream,
- ReactVersion as version,
+ version,
};
diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js b/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js
new file mode 100644
index 0000000000000..27e41b42e43ae
--- /dev/null
+++ b/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import ReactVersion from 'shared/ReactVersion';
+
+import type {ReactNodeList} from 'shared/ReactTypes';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+ abort,
+} from 'react-server/src/ReactFizzServer';
+
+import {
+ createResponseState,
+ createRootFormatContext,
+} from './ReactDOMServerLegacyFormatConfig';
+
+type ServerOptions = {
+ identifierPrefix?: string,
+};
+
+function onError() {
+ // Non-fatal errors are ignored.
+}
+
+function renderToStringImpl(
+ children: ReactNodeList,
+ options: void | ServerOptions,
+ generateStaticMarkup: boolean,
+ abortReason: string,
+): string {
+ let didFatal = false;
+ let fatalError = null;
+ let result = '';
+ const destination = {
+ push(chunk) {
+ if (chunk !== null) {
+ result += chunk;
+ }
+ return true;
+ },
+ destroy(error) {
+ didFatal = true;
+ fatalError = error;
+ },
+ };
+
+ let readyToStream = false;
+ function onShellReady() {
+ readyToStream = true;
+ }
+ const request = createRequest(
+ children,
+ createResponseState(
+ generateStaticMarkup,
+ options ? options.identifierPrefix : undefined,
+ ),
+ createRootFormatContext(),
+ Infinity,
+ onError,
+ undefined,
+ onShellReady,
+ undefined,
+ undefined,
+ );
+ startWork(request);
+ // If anything suspended and is still pending, we'll abort it before writing.
+ // That way we write only client-rendered boundaries from the start.
+ abort(request, abortReason);
+ startFlowing(request, destination);
+ if (didFatal) {
+ throw fatalError;
+ }
+
+ if (!readyToStream) {
+ // Note: This error message is the one we use on the client. It doesn't
+ // really make sense here. But this is the legacy server renderer, anyway.
+ // We're going to delete it soon.
+ throw new Error(
+ 'A component suspended while responding to synchronous input. This ' +
+ 'will cause the UI to be replaced with a loading indicator. To fix, ' +
+ 'updates that suspend should be wrapped with startTransition.',
+ );
+ }
+
+ return result;
+}
+
+export {renderToStringImpl, ReactVersion as version};
diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerNode.classic.fb.js b/packages/react-dom/src/server/ReactDOMLegacyServerNode.classic.fb.js
new file mode 100644
index 0000000000000..f542d77e295ab
--- /dev/null
+++ b/packages/react-dom/src/server/ReactDOMLegacyServerNode.classic.fb.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export {
+ renderToString,
+ renderToStaticMarkup,
+ version,
+} from './ReactDOMServerLegacyPartialRendererBrowser';
+
+export {
+ renderToNodeStream,
+ renderToStaticNodeStream,
+} from './ReactDOMLegacyServerNodeStream';
diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerNode.js b/packages/react-dom/src/server/ReactDOMLegacyServerNode.js
index f5c6aa1f46005..20e89de8b430e 100644
--- a/packages/react-dom/src/server/ReactDOMLegacyServerNode.js
+++ b/packages/react-dom/src/server/ReactDOMLegacyServerNode.js
@@ -9,104 +9,38 @@
import type {ReactNodeList} from 'shared/ReactTypes';
-import type {Request} from 'react-server/src/ReactFizzServer';
-
-import {
- createRequest,
- startWork,
- startFlowing,
- abort,
-} from 'react-server/src/ReactFizzServer';
-
+import {version, renderToStringImpl} from './ReactDOMLegacyServerImpl';
import {
- createResponseState,
- createRootFormatContext,
-} from './ReactDOMServerLegacyFormatConfig';
-
-import {
- version,
- renderToString,
- renderToStaticMarkup,
-} from './ReactDOMLegacyServerBrowser';
-
-import {Readable} from 'stream';
+ renderToNodeStream,
+ renderToStaticNodeStream,
+} from './ReactDOMLegacyServerNodeStream';
type ServerOptions = {
identifierPrefix?: string,
};
-class ReactMarkupReadableStream extends Readable {
- request: Request;
- startedFlowing: boolean;
- constructor() {
- // Calls the stream.Readable(options) constructor. Consider exposing built-in
- // features like highWaterMark in the future.
- super({});
- this.request = (null: any);
- this.startedFlowing = false;
- }
-
- _destroy(err, callback) {
- abort(this.request);
- // $FlowFixMe: The type definition for the callback should allow undefined and null.
- callback(err);
- }
-
- _read(size) {
- if (this.startedFlowing) {
- startFlowing(this.request, this);
- }
- }
-}
-
-function onError() {
- // Non-fatal errors are ignored.
-}
-
-function renderToNodeStreamImpl(
+function renderToString(
children: ReactNodeList,
- options: void | ServerOptions,
- generateStaticMarkup: boolean,
-): Readable {
- function onAllReady() {
- // We wait until everything has loaded before starting to write.
- // That way we only end up with fully resolved HTML even if we suspend.
- destination.startedFlowing = true;
- startFlowing(request, destination);
- }
- const destination = new ReactMarkupReadableStream();
- const request = createRequest(
+ options?: ServerOptions,
+): string {
+ return renderToStringImpl(
children,
- createResponseState(false, options ? options.identifierPrefix : undefined),
- createRootFormatContext(),
- Infinity,
- onError,
- onAllReady,
- undefined,
- undefined,
+ options,
+ false,
+ 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server',
);
- destination.request = request;
- startWork(request);
- return destination;
}
-function renderToNodeStream(
+function renderToStaticMarkup(
children: ReactNodeList,
options?: ServerOptions,
-): Readable {
- if (__DEV__) {
- console.error(
- 'renderToNodeStream is deprecated. Use renderToPipeableStream instead.',
- );
- }
- return renderToNodeStreamImpl(children, options, false);
-}
-
-function renderToStaticNodeStream(
- children: ReactNodeList,
- options?: ServerOptions,
-): Readable {
- return renderToNodeStreamImpl(children, options, true);
+): string {
+ return renderToStringImpl(
+ children,
+ options,
+ true,
+ 'The server used "renderToStaticMarkup" which does not support Suspense. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server',
+ );
}
export {
diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js b/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js
new file mode 100644
index 0000000000000..25b88156e0755
--- /dev/null
+++ b/packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactNodeList} from 'shared/ReactTypes';
+
+import type {Request} from 'react-server/src/ReactFizzServer';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+ abort,
+} from 'react-server/src/ReactFizzServer';
+
+import {
+ createResponseState,
+ createRootFormatContext,
+} from './ReactDOMServerLegacyFormatConfig';
+
+import {Readable} from 'stream';
+
+type ServerOptions = {
+ identifierPrefix?: string,
+};
+
+class ReactMarkupReadableStream extends Readable {
+ request: Request;
+ startedFlowing: boolean;
+ constructor() {
+ // Calls the stream.Readable(options) constructor. Consider exposing built-in
+ // features like highWaterMark in the future.
+ super({});
+ this.request = (null: any);
+ this.startedFlowing = false;
+ }
+
+ _destroy(err, callback) {
+ abort(this.request);
+ // $FlowFixMe: The type definition for the callback should allow undefined and null.
+ callback(err);
+ }
+
+ _read(size) {
+ if (this.startedFlowing) {
+ startFlowing(this.request, this);
+ }
+ }
+}
+
+function onError() {
+ // Non-fatal errors are ignored.
+}
+
+function renderToNodeStreamImpl(
+ children: ReactNodeList,
+ options: void | ServerOptions,
+ generateStaticMarkup: boolean,
+): Readable {
+ function onAllReady() {
+ // We wait until everything has loaded before starting to write.
+ // That way we only end up with fully resolved HTML even if we suspend.
+ destination.startedFlowing = true;
+ startFlowing(request, destination);
+ }
+ const destination = new ReactMarkupReadableStream();
+ const request = createRequest(
+ children,
+ createResponseState(false, options ? options.identifierPrefix : undefined),
+ createRootFormatContext(),
+ Infinity,
+ onError,
+ onAllReady,
+ undefined,
+ undefined,
+ );
+ destination.request = request;
+ startWork(request);
+ return destination;
+}
+
+function renderToNodeStream(
+ children: ReactNodeList,
+ options?: ServerOptions,
+): Readable {
+ if (__DEV__) {
+ console.error(
+ 'renderToNodeStream is deprecated. Use renderToPipeableStream instead.',
+ );
+ }
+ return renderToNodeStreamImpl(children, options, false);
+}
+
+function renderToStaticNodeStream(
+ children: ReactNodeList,
+ options?: ServerOptions,
+): Readable {
+ return renderToNodeStreamImpl(children, options, true);
+}
+
+export {renderToNodeStream, renderToStaticNodeStream};
diff --git a/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js
index 857a374f3e81a..53de82a1ec288 100644
--- a/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js
+++ b/packages/react-server-dom-relay/src/__tests__/ReactDOMServerFB-test.internal.js
@@ -192,7 +192,7 @@ describe('ReactDOMServerFB', () => {
expect(remaining).toEqual('');
expect(errors).toEqual([
- 'This Suspense boundary was aborted by the server.',
+ 'The render was aborted by the server without a reason.',
]);
});
});
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 2b0a3d82e0600..9e6551d938c0f 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -1530,10 +1530,9 @@ function abortTaskSoft(task: Task): void {
finishedTask(request, boundary, segment);
}
-function abortTask(task: Task): void {
+function abortTask(task: Task, request: Request, reason: mixed): void {
// This aborts the task and aborts the parent that it blocks, putting it into
// client rendered mode.
- const request: Request = this;
const boundary = task.blockedBoundary;
const segment = task.blockedSegment;
segment.status = ABORTED;
@@ -1553,12 +1552,27 @@ function abortTask(task: Task): void {
if (!boundary.forceClientRender) {
boundary.forceClientRender = true;
- const error = new Error(
- 'This Suspense boundary was aborted by the server.',
- );
+ let error =
+ reason === undefined
+ ? new Error('The render was aborted by the server without a reason.')
+ : reason;
boundary.errorDigest = request.onError(error);
if (__DEV__) {
- captureBoundaryErrorDetailsDev(boundary, error);
+ const errorPrefix =
+ 'The server did not finish this Suspense boundary: ';
+ if (error && typeof error.message === 'string') {
+ error = errorPrefix + error.message;
+ } else {
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ error = errorPrefix + String(error);
+ }
+ const previousTaskInDev = currentTaskInDEV;
+ currentTaskInDEV = task;
+ try {
+ captureBoundaryErrorDetailsDev(boundary, error);
+ } finally {
+ currentTaskInDEV = previousTaskInDev;
+ }
}
if (boundary.parentFlushed) {
request.clientRenderedBoundaries.push(boundary);
@@ -1567,7 +1581,9 @@ function abortTask(task: Task): void {
// If this boundary was still pending then we haven't already cancelled its fallbacks.
// We'll need to abort the fallbacks, which will also error that parent boundary.
- boundary.fallbackAbortableTasks.forEach(abortTask, request);
+ boundary.fallbackAbortableTasks.forEach(fallbackTask =>
+ abortTask(fallbackTask, request, reason),
+ );
boundary.fallbackAbortableTasks.clear();
request.allPendingTasks--;
@@ -2159,10 +2175,10 @@ export function startFlowing(request: Request, destination: Destination): void {
}
// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
-export function abort(request: Request): void {
+export function abort(request: Request, reason: mixed): void {
try {
const abortableTasks = request.abortableTasks;
- abortableTasks.forEach(abortTask, request);
+ abortableTasks.forEach(task => abortTask(task, request, reason));
abortableTasks.clear();
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 00748befe6d81..48afd882a0c79 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -417,7 +417,7 @@
"429": "ServerContext: %s already defined",
"430": "ServerContext can only have a value prop and children. Found: %s",
"431": "React elements are not allowed in ServerContext",
- "432": "This Suspense boundary was aborted by the server.",
+ "432": "The render was aborted by the server without a reason.",
"433": "useId can only be used while React is rendering",
"434": "`dangerouslySetInnerHTML` does not make sense on
."
-}
+}
\ No newline at end of file
diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js
index 4e47b4fd6ffcb..eb0eef9109837 100644
--- a/scripts/shared/inlinedHostConfigs.js
+++ b/scripts/shared/inlinedHostConfigs.js
@@ -69,8 +69,11 @@ module.exports = [
paths: [
'react-dom',
'react-server-dom-webpack',
+ 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Brower and *Node files
'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser
'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node
+ 'react-dom/src/server/ReactDOMLegacyServerNode.classic.fb.js',
+ 'react-dom/src/server/ReactDOMLegacyServerNodeStream.js', // file indirection to support partial forking of some methods in *Node
'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
],
isFlowTyped: true,