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;