Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Land enableSuspenseServerRenderer flag #24405

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 0 additions & 37 deletions packages/react-dom/src/__tests__/ReactServerRendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ let React;
let ReactDOMServer;
let PropTypes;
let ReactCurrentDispatcher;
const enableSuspenseServerRenderer = require('shared/ReactFeatureFlags')
.enableSuspenseServerRenderer;

describe('ReactDOMServer', () => {
beforeEach(() => {
Expand Down Expand Up @@ -678,41 +676,6 @@ describe('ReactDOMServer', () => {
expect(markup).toBe('<div></div>');
});

if (!enableSuspenseServerRenderer) {
it('throws for unsupported types on the server', () => {
expect(() => {
ReactDOMServer.renderToString(<React.Suspense />);
}).toThrow('ReactDOMServer does not yet support Suspense.');

async function fakeImport(result) {
return {default: result};
}

expect(() => {
const LazyFoo = React.lazy(() =>
fakeImport(
new Promise(resolve =>
resolve(function Foo() {
return <div />;
}),
),
),
);
ReactDOMServer.renderToString(<LazyFoo />);
}).toThrow('ReactDOMServer does not yet support Suspense.');
});

it('throws when suspending on the server', () => {
function AsyncFoo() {
throw new Promise(() => {});
}

expect(() => {
ReactDOMServer.renderToString(<AsyncFoo />);
}).toThrow('ReactDOMServer does not yet support Suspense.');
});
}

it('does not get confused by throwing null', () => {
function Bad() {
// eslint-disable-next-line no-throw-literal
Expand Down
25 changes: 11 additions & 14 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';

import {
enableClientRenderFallbackOnHydrationMismatch,
enableSuspenseServerRenderer,
enableCreateEventHandleAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
Expand Down Expand Up @@ -747,19 +746,17 @@ function getNextHydratable(node) {
if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
break;
}
if (enableSuspenseServerRenderer) {
if (nodeType === COMMENT_NODE) {
const nodeData = (node: any).data;
if (
nodeData === SUSPENSE_START_DATA ||
nodeData === SUSPENSE_FALLBACK_START_DATA ||
nodeData === SUSPENSE_PENDING_START_DATA
) {
break;
}
if (nodeData === SUSPENSE_END_DATA) {
return null;
}
if (nodeType === COMMENT_NODE) {
const nodeData = (node: any).data;
if (
nodeData === SUSPENSE_START_DATA ||
nodeData === SUSPENSE_FALLBACK_START_DATA ||
nodeData === SUSPENSE_PENDING_START_DATA
) {
break;
}
if (nodeData === SUSPENSE_END_DATA) {
return null;
}
}
}
Expand Down
85 changes: 38 additions & 47 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
warnAboutDeprecatedLifecycles,
disableLegacyContext,
disableModulePatternComponents,
enableSuspenseServerRenderer,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {
Expand Down Expand Up @@ -965,21 +964,17 @@ class ReactDOMServerRenderer {
outBuffer += this.render(child, frame.context, frame.domNamespace);
} catch (err) {
if (err != null && typeof err.then === 'function') {
if (enableSuspenseServerRenderer) {
if (this.suspenseDepth <= 0) {
throw new Error(
// 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 {
throw new Error('ReactDOMServer does not yet support Suspense.');
if (this.suspenseDepth <= 0) {
throw new Error(
// 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 {
throw err;
}
Expand Down Expand Up @@ -1097,39 +1092,35 @@ class ReactDOMServerRenderer {
return '';
}
case REACT_SUSPENSE_TYPE: {
if (enableSuspenseServerRenderer) {
const fallback = ((nextChild: any): ReactElement).props.fallback;
const fallbackChildren = toArray(fallback);
const nextChildren = toArray(
((nextChild: any): ReactElement).props.children,
);
const fallbackFrame: Frame = {
type: null,
domNamespace: parentNamespace,
children: fallbackChildren,
childIndex: 0,
context: context,
footer: '<!--/$-->',
};
const frame: Frame = {
fallbackFrame,
type: REACT_SUSPENSE_TYPE,
domNamespace: parentNamespace,
children: nextChildren,
childIndex: 0,
context: context,
footer: '<!--/$-->',
};
if (__DEV__) {
((frame: any): FrameDev).debugElementStack = [];
((fallbackFrame: any): FrameDev).debugElementStack = [];
}
this.stack.push(frame);
this.suspenseDepth++;
return '<!--$-->';
} else {
throw new Error('ReactDOMServer does not yet support Suspense.');
const fallback = ((nextChild: any): ReactElement).props.fallback;
const fallbackChildren = toArray(fallback);
const nextChildren = toArray(
((nextChild: any): ReactElement).props.children,
);
const fallbackFrame: Frame = {
type: null,
domNamespace: parentNamespace,
children: fallbackChildren,
childIndex: 0,
context: context,
footer: '<!--/$-->',
};
const frame: Frame = {
fallbackFrame,
type: REACT_SUSPENSE_TYPE,
domNamespace: parentNamespace,
children: nextChildren,
childIndex: 0,
context: context,
footer: '<!--/$-->',
};
if (__DEV__) {
((frame: any): FrameDev).debugElementStack = [];
((fallbackFrame: any): FrameDev).debugElementStack = [];
}
this.stack.push(frame);
this.suspenseDepth++;
return '<!--$-->';
}
// eslint-disable-next-line-no-fallthrough
case REACT_SCOPE_TYPE: {
Expand Down
151 changes: 72 additions & 79 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ import {
disableModulePatternComponents,
enableProfilerCommitHooks,
enableProfilerTimer,
enableSuspenseServerRenderer,
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableCache,
Expand Down Expand Up @@ -2134,17 +2133,15 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
// If we're currently hydrating, try to hydrate this boundary.
tryToClaimNextHydratableInstance(workInProgress);
// This could've been a dehydrated suspense component.
if (enableSuspenseServerRenderer) {
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
if (suspenseState !== null) {
const dehydrated = suspenseState.dehydrated;
if (dehydrated !== null) {
return mountDehydratedSuspenseComponent(
workInProgress,
dehydrated,
renderLanes,
);
}
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
if (suspenseState !== null) {
const dehydrated = suspenseState.dehydrated;
if (dehydrated !== null) {
return mountDehydratedSuspenseComponent(
workInProgress,
dehydrated,
renderLanes,
);
}
}

Expand Down Expand Up @@ -2220,59 +2217,57 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
// The current tree is already showing a fallback

// Special path for hydration
if (enableSuspenseServerRenderer) {
const dehydrated = prevState.dehydrated;
if (dehydrated !== null) {
if (!didSuspend) {
return updateDehydratedSuspenseComponent(
current,
workInProgress,
dehydrated,
prevState,
renderLanes,
);
} else if (workInProgress.flags & ForceClientRender) {
// Something errored during hydration. Try again without hydrating.
workInProgress.flags &= ~ForceClientRender;
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
new Error(
'There was an error while hydrating this Suspense boundary. ' +
'Switched to client rendering.',
),
);
} else if (
(workInProgress.memoizedState: null | SuspenseState) !== null
) {
// Something suspended and we should still be in dehydrated mode.
// Leave the existing child in place.
workInProgress.child = current.child;
// The dehydrated completion pass expects this flag to be there
// but the normal suspense pass doesn't.
workInProgress.flags |= DidCapture;
return null;
} else {
// Suspended but we should no longer be in dehydrated mode.
// Therefore we now have to render the fallback.
renderDidSuspendDelayIfPossible();
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackChildFragment;
}
const dehydrated = prevState.dehydrated;
if (dehydrated !== null) {
if (!didSuspend) {
return updateDehydratedSuspenseComponent(
current,
workInProgress,
dehydrated,
prevState,
renderLanes,
);
} else if (workInProgress.flags & ForceClientRender) {
// Something errored during hydration. Try again without hydrating.
workInProgress.flags &= ~ForceClientRender;
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
new Error(
'There was an error while hydrating this Suspense boundary. ' +
'Switched to client rendering.',
),
);
} else if (
(workInProgress.memoizedState: null | SuspenseState) !== null
) {
// Something suspended and we should still be in dehydrated mode.
// Leave the existing child in place.
workInProgress.child = current.child;
// The dehydrated completion pass expects this flag to be there
// but the normal suspense pass doesn't.
workInProgress.flags |= DidCapture;
return null;
} else {
// Suspended but we should no longer be in dehydrated mode.
// Therefore we now have to render the fallback.
renderDidSuspendDelayIfPossible();
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackChildFragment;
}
}

Expand Down Expand Up @@ -3657,20 +3652,18 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
if (state !== null) {
if (enableSuspenseServerRenderer) {
if (state.dehydrated !== null) {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.flags |= DidCapture;
// We should never render the children of a dehydrated boundary until we
// upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
return null;
}
if (state.dehydrated !== null) {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.flags |= DidCapture;
// We should never render the children of a dehydrated boundary until we
// upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
return null;
}

// If this boundary is currently timed out, we need to decide
Expand Down