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." }