diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index a55b8774c0ac..dd1c7548c403 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1698,6 +1698,99 @@ describe('ReactDOMServerPartialHydration', () => { expect(container.textContent).toBe('ABC'); }); + // @gate experimental + it('clears server boundaries when SuspenseList does a second pass', async () => { + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + const ref = React.createRef(); + + function Child({children}) { + if (suspend) { + throw promise; + } else { + return children; + } + } + + function Before() { + Scheduler.unstable_yieldValue('Before'); + return null; + } + + function After() { + Scheduler.unstable_yieldValue('After'); + return null; + } + + function FirstRow() { + return ( + <> + + + A + + + + ); + } + + function App() { + return ( + + + + + + B + + + + + ); + } + + suspend = false; + const html = ReactDOMServer.renderToString(); + expect(Scheduler).toHaveYielded(['Before', 'After']); + + const container = document.createElement('div'); + container.innerHTML = html; + + const b = container.getElementsByTagName('span')[1]; + expect(b.textContent).toBe('B'); + + const root = ReactDOM.createRoot(container, {hydrate: true}); + + // Increase hydration priority to higher than "offscreen". + ReactDOM.unstable_scheduleHydration(b); + + suspend = true; + + await act(async () => { + root.render(); + expect(Scheduler).toFlushAndYieldThrough(['Before']); + // This took a long time to render. + Scheduler.unstable_advanceTime(1000); + expect(Scheduler).toFlushAndYield(['After']); + // This will cause us to skip the second row completely. + }); + + // We haven't hydrated the second child but the placeholder is still in the list. + expect(ref.current).toBe(null); + expect(container.textContent).toBe('AB'); + + suspend = false; + await act(async () => { + // Resolve the boundary to be in its resolved final state. + await resolve(); + }); + + expect(container.textContent).toBe('AB'); + expect(ref.current).toBe(b); + }); + // @gate experimental it('can client render nested boundaries', async () => { let suspend = false; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index e420bb6916c7..577224097c24 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -118,6 +118,7 @@ import { prepareToHydrateHostSuspenseInstance, popHydrationState, resetHydrationState, + getIsHydrating, } from './ReactFiberHydrationContext.new'; import { enableSchedulerTracing, @@ -579,6 +580,11 @@ function cutOffTailIfNeeded( renderState: SuspenseListRenderState, hasRenderedATailFallback: boolean, ) { + if (getIsHydrating()) { + // If we're hydrating, we should consume as many items as we can + // so we don't leave any behind. + return; + } switch (renderState.tailMode) { case 'hidden': { // Any insertions at the end of the tail list after this point diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index d024c9711ce1..cc7ed6017ed7 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -115,6 +115,7 @@ import { prepareToHydrateHostSuspenseInstance, popHydrationState, resetHydrationState, + getIsHydrating, } from './ReactFiberHydrationContext.old'; import { enableSchedulerTracing, @@ -575,6 +576,11 @@ function cutOffTailIfNeeded( renderState: SuspenseListRenderState, hasRenderedATailFallback: boolean, ) { + if (getIsHydrating()) { + // If we're hydrating, we should consume as many items as we can + // so we don't leave any behind. + return; + } switch (renderState.tailMode) { case 'hidden': { // Any insertions at the end of the tail list after this point