Compare commits

...

3 Commits
leg ... portals

4 changed files with 93 additions and 25 deletions

View File

@@ -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 (
<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', () => {
const div = document.createElement('div');
expect(() => {
ReactDOMServer.renderToString(
<div>{ReactDOM.createPortal(<div />, div)}</div>,
if (!__EXPERIMENTAL__) {
it('should throw rendering portals on the server', () => {
const div = document.createElement('div');
expect(() => {
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', () => {
class ComponentWithWarning extends React.Component {

View File

@@ -900,16 +900,29 @@ 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 <Suspense fallback=...> component higher in the tree to ' +
'provide a loading indicator or placeholder to display.',
);
if (err === REACT_PORTAL_TYPE) {
invariant(
this.suspenseDepth > 0,
'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.',
);
} 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;
} else {
invariant(false, 'ReactDOMServer does not yet support Suspense.');
@@ -961,11 +974,17 @@ 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.',
);
if ($$typeof === REACT_PORTAL_TYPE) {
if (enableSuspenseServerRenderer) {
throw $$typeof;
} 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.
invariant(
false,

View File

@@ -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 <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."
}