diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 7a25979987..6966d96dbc 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -78,6 +78,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactFeatureFlags.enableSuspenseCallback = true;
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
+ ReactFeatureFlags.enableSuspenseServerRenderer = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -2563,4 +2564,49 @@ describe('ReactDOMServerPartialHydration', () => {
// Now we're hydrated.
expect(ref.current).not.toBe(null);
});
+
+ it('should return fallback during server rendering when Portal has a suspense boundary ', () => {
+ const portalContainer = document.createElement('div');
+ function App() {
+ return (
+
+
+ {ReactDOM.createPortal({'Portal'}
, portalContainer)}
+
+
+ );
+ }
+
+ const markup = ReactDOMServer.renderToString();
+ expect(markup).toContain('Loading...');
+ expect(portalContainer.textContent).toBe('');
+
+ const container = document.createElement('div');
+ container.innerHTML = markup;
+
+ // Hydrating this cames the fallback to be removed and the Portal to be added.
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ act(() => {
+ root.render();
+ });
+ expect(portalContainer.textContent).toBe('Portal');
+ expect(container.textContent).toBe('');
+ });
+
+ it('should error during server rendering when Portal does not have a Suspense boundary', () => {
+ const portalContainer = document.createElement('div');
+ function App() {
+ return (
+
+ {ReactDOM.createPortal(
{'Portal'}
, portalContainer)}
+
+ );
+ }
+
+ expect(() => ReactDOMServer.renderToString()).toThrow(
+ 'Portals must have a fallback UI when being rendered on the server.\n\n' +
+ 'Add a component higher in the tree to provide a ' +
+ 'loading indicator or placeholder to display.',
+ );
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
index 1dcc050de9..6dac28bb88 100644
--- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
@@ -324,17 +324,19 @@ describe('ReactDOMServerHydration', () => {
);
});
- it('should throw rendering portals on the server', () => {
- const div = document.createElement('div');
- expect(() => {
- ReactDOMServer.renderToString(
- {ReactDOM.createPortal(
, div)}
,
+ if (!__EXPERIMENTAL__) {
+ it('should throw rendering portals on the server', () => {
+ const div = document.createElement('div');
+ expect(() => {
+ ReactDOMServer.renderToString(
+ {ReactDOM.createPortal(
, div)}
,
+ );
+ }).toThrow(
+ 'Portals are not currently supported by the server renderer. ' +
+ 'Render them conditionally so that they only appear on the client render.',
);
- }).toThrow(
- 'Portals are not currently supported by the server renderer. ' +
- 'Render them conditionally so that they only appear on the client render.',
- );
- });
+ });
+ }
it('should be able to render and hydrate Mode components', () => {
class ComponentWithWarning extends React.Component {
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index ecee219346..54e93d6382 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -900,16 +900,30 @@ class ReactDOMServerRenderer {
try {
outBuffer += this.render(child, frame.context, frame.domNamespace);
} catch (err) {
- if (err != null && typeof err.then === 'function') {
+ if (
+ err != null &&
+ (typeof err.then === 'function' || err === REACT_PORTAL_TYPE)
+ ) {
if (enableSuspenseServerRenderer) {
- invariant(
- this.suspenseDepth > 0,
- // TODO: include component name. This is a bit tricky with current factoring.
- 'A React component suspended while rendering, but no fallback UI was specified.\n' +
- '\n' +
- 'Add a component higher in the tree to ' +
- 'provide a loading indicator or placeholder to display.',
- );
+ if (err === REACT_PORTAL_TYPE) {
+ invariant(
+ this.suspenseDepth > 0,
+ // TODO: include component name. This is a bit tricky with current factoring.
+ 'Portals must have a fallback UI when being rendered on the server.\n' +
+ '\n' +
+ 'Add a component higher in the tree to ' +
+ 'provide a loading indicator or placeholder to display.',
+ );
+ } else {
+ invariant(
+ this.suspenseDepth > 0,
+ // TODO: include component name. This is a bit tricky with current factoring.
+ 'A React component suspended while rendering, but no fallback UI was specified.\n' +
+ '\n' +
+ 'Add a component higher in the tree to ' +
+ 'provide a loading indicator or placeholder to display.',
+ );
+ }
suspended = true;
} else {
invariant(false, 'ReactDOMServer does not yet support Suspense.');
@@ -961,11 +975,18 @@ class ReactDOMServerRenderer {
if (nextChild != null && nextChild.$$typeof != null) {
// Catch unexpected special types early.
const $$typeof = nextChild.$$typeof;
- invariant(
- $$typeof !== REACT_PORTAL_TYPE,
- 'Portals are not currently supported by the server renderer. ' +
- 'Render them conditionally so that they only appear on the client render.',
- );
+ // LUNA
+ if ($$typeof === REACT_PORTAL_TYPE) {
+ if (enableSuspenseServerRenderer) {
+ throw $$typeof;
+ } else {
+ invariant(
+ $$typeof !== REACT_PORTAL_TYPE,
+ 'Portals are not currently supported by the server renderer. ' +
+ 'Render them conditionally so that they only appear on the client render.',
+ );
+ }
+ }
// Catch-all to prevent an infinite loop if React.Children.toArray() supports some new type.
invariant(
false,
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index f0f93e9b69..18fe13907c 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -344,5 +344,6 @@
"343": "ReactDOMServer does not yet support scope components.",
"344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.",
"345": "Root did not complete. This is a bug in React.",
- "346": "An event responder context was used outside of an event cycle."
+ "346": "An event responder context was used outside of an event cycle.",
+ "347": "Portals must have a fallback UI when being rendered on the server.\n\nAdd a component higher in the tree to provide a loading indicator or placeholder to display."
}