Compare commits

...

3 Commits

4 changed files with 93 additions and 25 deletions

View File

@@ -78,6 +78,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactFeatureFlags.enableSuspenseCallback = true; ReactFeatureFlags.enableSuspenseCallback = true;
ReactFeatureFlags.enableDeprecatedFlareAPI = true; ReactFeatureFlags.enableDeprecatedFlareAPI = true;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableSuspenseServerRenderer = true;
React = require('react'); React = require('react');
ReactDOM = require('react-dom'); ReactDOM = require('react-dom');
@@ -2563,4 +2564,49 @@ describe('ReactDOMServerPartialHydration', () => {
// Now we're hydrated. // Now we're hydrated.
expect(ref.current).not.toBe(null); 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 (
<div>
<React.Suspense fallback={'Loading...'}>
{ReactDOM.createPortal(<div>{'Portal'}</div>, portalContainer)}
</React.Suspense>
</div>
);
}
const markup = ReactDOMServer.renderToString(<App />);
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(<App />);
});
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 (
<div>
{ReactDOM.createPortal(<div>{'Portal'}</div>, portalContainer)}
</div>
);
}
expect(() => ReactDOMServer.renderToString(<App />)).toThrow(
'Portals must have a fallback UI when being rendered on the server.\n\n' +
'Add a <Suspense fallback=...> component higher in the tree to provide a ' +
'loading indicator or placeholder to display.',
);
});
}); });

View File

@@ -324,17 +324,19 @@ describe('ReactDOMServerHydration', () => {
); );
}); });
it('should throw rendering portals on the server', () => { if (!__EXPERIMENTAL__) {
const div = document.createElement('div'); it('should throw rendering portals on the server', () => {
expect(() => { const div = document.createElement('div');
ReactDOMServer.renderToString( expect(() => {
<div>{ReactDOM.createPortal(<div />, div)}</div>, ReactDOMServer.renderToString(
<div>{ReactDOM.createPortal(<div />, div)}</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', () => { it('should be able to render and hydrate Mode components', () => {
class ComponentWithWarning extends React.Component { class ComponentWithWarning extends React.Component {

View File

@@ -900,16 +900,29 @@ class ReactDOMServerRenderer {
try { try {
outBuffer += this.render(child, frame.context, frame.domNamespace); outBuffer += this.render(child, frame.context, frame.domNamespace);
} catch (err) { } catch (err) {
if (err != null && typeof err.then === 'function') { if (
err != null &&
(typeof err.then === 'function' || err === REACT_PORTAL_TYPE)
) {
if (enableSuspenseServerRenderer) { if (enableSuspenseServerRenderer) {
invariant( if (err === REACT_PORTAL_TYPE) {
this.suspenseDepth > 0, invariant(
// TODO: include component name. This is a bit tricky with current factoring. this.suspenseDepth > 0,
'A React component suspended while rendering, but no fallback UI was specified.\n' + 'Portals must have a fallback UI when being rendered on the server.\n' +
'\n' + '\n' +
'Add a <Suspense fallback=...> component higher in the tree to ' + 'Add a <Suspense fallback=...> component higher in the tree to ' +
'provide a loading indicator or placeholder to display.', '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 <Suspense fallback=...> component higher in the tree to ' +
'provide a loading indicator or placeholder to display.',
);
}
suspended = true; suspended = true;
} else { } else {
invariant(false, 'ReactDOMServer does not yet support Suspense.'); invariant(false, 'ReactDOMServer does not yet support Suspense.');
@@ -961,11 +974,17 @@ class ReactDOMServerRenderer {
if (nextChild != null && nextChild.$$typeof != null) { if (nextChild != null && nextChild.$$typeof != null) {
// Catch unexpected special types early. // Catch unexpected special types early.
const $$typeof = nextChild.$$typeof; const $$typeof = nextChild.$$typeof;
invariant( if ($$typeof === REACT_PORTAL_TYPE) {
$$typeof !== REACT_PORTAL_TYPE, if (enableSuspenseServerRenderer) {
'Portals are not currently supported by the server renderer. ' + throw $$typeof;
'Render them conditionally so that they only appear on the client render.', } else {
); invariant(
false,
'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. // Catch-all to prevent an infinite loop if React.Children.toArray() supports some new type.
invariant( invariant(
false, false,

View File

@@ -344,5 +344,6 @@
"343": "ReactDOMServer does not yet support scope components.", "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.", "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.", "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 <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."
} }