diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index bd41a9361e..01114efa5c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -2809,4 +2809,93 @@ describe('ReactDOMServerPartialHydration', () => {
// Now we're hydrated.
expect(ref.current).not.toBe(null);
});
+
+ // @gate experimental
+ // @gate new
+ it('renders a hidden LegacyHidden component', async () => {
+ const LegacyHidden = React.unstable_LegacyHidden;
+
+ const ref = React.createRef();
+
+ function App() {
+ return (
+
+ Hidden child
+
+ );
+ }
+
+ const finalHTML = ReactDOMServer.renderToString();
+
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ const span = container.getElementsByTagName('span')[0];
+ expect(span).toBe(undefined);
+
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render();
+ Scheduler.unstable_flushAll();
+ expect(ref.current.innerHTML).toBe('Hidden child');
+ });
+
+ // @gate experimental
+ // @gate new
+ it('renders a hidden LegacyHidden component inside a Suspense boundary', async () => {
+ const LegacyHidden = React.unstable_LegacyHidden;
+
+ const ref = React.createRef();
+
+ function App() {
+ return (
+
+
+ Hidden child
+
+
+ );
+ }
+
+ const finalHTML = ReactDOMServer.renderToString();
+
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ const span = container.getElementsByTagName('span')[0];
+ expect(span).toBe(undefined);
+
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render();
+ Scheduler.unstable_flushAll();
+ expect(ref.current.innerHTML).toBe('Hidden child');
+ });
+
+ // @gate experimental
+ // @gate new
+ it('renders a visible LegacyHidden component', async () => {
+ const LegacyHidden = React.unstable_LegacyHidden;
+
+ const ref = React.createRef();
+
+ function App() {
+ return (
+
+ Hidden child
+
+ );
+ }
+
+ const finalHTML = ReactDOMServer.renderToString();
+
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ const span = container.getElementsByTagName('span')[0];
+
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+ root.render();
+ Scheduler.unstable_flushAll();
+ expect(ref.current).toBe(span);
+ expect(ref.current.innerHTML).toBe('Hidden child');
+ });
});
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index 4615bd8e28..b2ef1778e3 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -42,6 +42,7 @@ import {
REACT_MEMO_TYPE,
REACT_FUNDAMENTAL_TYPE,
REACT_SCOPE_TYPE,
+ REACT_LEGACY_HIDDEN_TYPE,
} from 'shared/ReactSymbols';
import {
@@ -1019,6 +1020,18 @@ class ReactDOMServerRenderer {
}
switch (elementType) {
+ case REACT_LEGACY_HIDDEN_TYPE: {
+ if (!enableSuspenseServerRenderer) {
+ break;
+ }
+ if (((nextChild: any): ReactElement).props.mode === 'hidden') {
+ // In hidden mode, render nothing.
+ return '';
+ }
+ // Otherwise the tree is visible, so act like a fragment.
+ }
+ // Intentional fall through
+ // eslint-disable-next-line no-fallthrough
case REACT_DEBUG_TRACING_MODE_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 2058bf403a..76838eda06 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -165,6 +165,7 @@ import {
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
tryToClaimNextHydratableInstance,
+ getIsHydrating,
warnIfHydrating,
} from './ReactFiberHydrationContext.new';
import {
@@ -574,7 +575,13 @@ function updateOffscreenComponent(
};
workInProgress.memoizedState = nextState;
pushRenderLanes(workInProgress, renderLanes);
- } else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
+ } else if (
+ !includesSomeLane(renderLanes, (OffscreenLane: Lane)) ||
+ // Server renderer does not render hidden subtrees, so if we're hydrating
+ // we should always bail out and schedule a subsequent render pass, to
+ // force a client render. Even if we're already at Offscreen priority.
+ (current === null && getIsHydrating())
+ ) {
let nextBaseLanes;
if (prevState !== null) {
const prevBaseLanes = prevState.baseLanes;