diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 6080a26964..419dd5a587 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -587,6 +587,80 @@ describe('ReactDOMServerPartialHydration', () => { expect(span.className).toBe('hi'); }); + // @gate experimental + it('blocks updates to hydrate the content first if props changed at idle priority', async () => { + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => (resolve = resolvePromise)); + const ref = React.createRef(); + + function Child({text}) { + if (suspend) { + throw promise; + } else { + return text; + } + } + + function App({text, className}) { + return ( +
+ + + + + +
+ ); + } + + suspend = false; + const finalHTML = ReactDOMServer.renderToString( + , + ); + const container = document.createElement('div'); + container.innerHTML = finalHTML; + + const span = container.getElementsByTagName('span')[0]; + + // On the client we don't have all data yet but we want to start + // hydrating anyway. + suspend = true; + const root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + expect(ref.current).toBe(null); + expect(span.textContent).toBe('Hello'); + + // Schedule an update at idle priority + Scheduler.unstable_runWithPriority(Scheduler.unstable_IdlePriority, () => { + root.render(); + }); + + // At the same time, resolving the promise so that rendering can complete. + suspend = false; + resolve(); + await promise; + + // This should first complete the hydration and then flush the update onto the hydrated state. + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + // The new span should be the same since we should have successfully hydrated + // before changing it. + const newSpan = container.getElementsByTagName('span')[0]; + expect(span).toBe(newSpan); + + // We should now have fully rendered with a ref on the new span. + expect(ref.current).toBe(span); + expect(span.textContent).toBe('Hi'); + // If we ended up hydrating the existing content, we won't have properly + // patched up the tree, which might mean we haven't patched the className. + expect(span.className).toBe('hi'); + }); + // @gate experimental it('shows the fallback if props have changed before hydration completes and is still suspended', async () => { let suspend = false; diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index d107913e10..72a6b015f3 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -208,6 +208,7 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { return_updateRangeEnd = IdleUpdateRangeStart; return IdleHydrationLane; } else { + return_highestLanePriority = IdleLanePriority; return_updateRangeEnd = IdleUpdateRangeEnd; return idleLanes; } @@ -527,7 +528,7 @@ export function findUpdateLane( // Should be handled by findTransitionLane instead break; case IdleLanePriority: - let lane = findLane(IdleUpdateRangeStart, IdleUpdateRangeEnd, IdleLanes); + let lane = findLane(IdleUpdateRangeStart, IdleUpdateRangeEnd, wipLanes); if (lane === NoLane) { lane = IdleHydrationLane; }