diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js index 4f8a4d98d654d..536200025c819 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js @@ -9,7 +9,10 @@ 'use strict'; -import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils'; +import { + insertNodesAndExecuteScripts, + getVisibleChildren, +} from '../test-utils/FizzTestUtils'; // Polyfills for test environment global.ReadableStream = @@ -17,20 +20,28 @@ global.ReadableStream = global.TextEncoder = require('util').TextEncoder; let act; +let assertLog; +let waitForPaint; let container; let React; +let Scheduler; let ReactDOMServer; let ReactDOMClient; let useDeferredValue; +let Suspense; describe('ReactDOMFizzForm', () => { beforeEach(() => { jest.resetModules(); React = require('react'); + Scheduler = require('scheduler'); ReactDOMServer = require('react-dom/server.browser'); ReactDOMClient = require('react-dom/client'); - useDeferredValue = require('react').useDeferredValue; + useDeferredValue = React.useDeferredValue; + Suspense = React.Suspense; act = require('internal-test-utils').act; + assertLog = require('internal-test-utils').assertLog; + waitForPaint = require('internal-test-utils').waitForPaint; container = document.createElement('div'); document.body.appendChild(container); }); @@ -54,6 +65,11 @@ describe('ReactDOMFizzForm', () => { insertNodesAndExecuteScripts(temp, container, null); } + function Text({text}) { + Scheduler.log(text); + return text; + } + // @gate enableUseDeferredValueInitialArg it('returns initialValue argument, if provided', async () => { function App() { @@ -68,4 +84,106 @@ describe('ReactDOMFizzForm', () => { await act(() => ReactDOMClient.hydrateRoot(container, )); expect(container.textContent).toEqual('Final'); }); + + // @gate enableUseDeferredValueInitialArg + it( + 'useDeferredValue during hydration has higher priority than remaining ' + + 'incremental hydration', + async () => { + function B() { + const text = useDeferredValue('B [Final]', 'B [Initial]'); + return ; + } + + function App() { + return ( +
+ + + + }> + + + +
+ }> + + + + +
+
+
+ ); + } + + const cRef = React.createRef(); + + // The server renders using the "initial" value for B. + const stream = await ReactDOMServer.renderToReadableStream(); + await readIntoContainer(stream); + assertLog(['A', 'B [Initial]', 'C']); + expect(getVisibleChildren(container)).toEqual( +
+ A + B [Initial] +
+ C +
+
, + ); + + const serverRenderedC = document.getElementById('C'); + + // On the client, we first hydrate the initial value, then upgrade + // to final. + await act(async () => { + ReactDOMClient.hydrateRoot(container, ); + + // First the outermost Suspense boundary hydrates. + await waitForPaint(['A']); + expect(cRef.current).toBe(null); + + // Then the next level hydrates. This level includes a useDeferredValue, + // so we should prioritize upgrading it before we proceed to hydrating + // additional levels. + await waitForPaint(['B [Initial]']); + expect(getVisibleChildren(container)).toEqual( +
+ A + B [Initial] +
+ C +
+
, + ); + expect(cRef.current).toBe(null); + + // This paint should only update B. C should still be dehydrated. + await waitForPaint(['B [Final]']); + expect(getVisibleChildren(container)).toEqual( +
+ A + B [Final] +
+ C +
+
, + ); + expect(cRef.current).toBe(null); + }); + // Finally we can hydrate C + assertLog(['C']); + expect(getVisibleChildren(container)).toEqual( +
+ A + B [Final] +
+ C +
+
, + ); + expect(cRef.current).toBe(serverRenderedC); + }, + ); }); diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 864edf6ee676b..75bf09dc01e25 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -730,8 +730,10 @@ function markSpawnedDeferredLane( root.entanglements[spawnedLaneIndex] |= DeferredLane | // If the parent render task suspended, we must also entangle those lanes - // with the spawned task. - entangledLanes; + // with the spawned task, so that the deferred task includes all the same + // updates that the parent task did. We can exclude any lane that is not + // used for updates (e.g. Offscreen). + (entangledLanes & UpdateLanes); } export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index fbfa153b71785..2473050cf51d1 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -83,7 +83,10 @@ import { resetWorkInProgress, } from './ReactFiber'; import {isRootDehydrated} from './ReactFiberShellHydration'; -import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext'; +import { + getIsHydrating, + didSuspendOrErrorWhileHydratingDEV, +} from './ReactFiberHydrationContext'; import { NoMode, ProfileMode, @@ -690,13 +693,21 @@ export function requestDeferredLane(): Lane { // If there are multiple useDeferredValue hooks in the same render, the // tasks that they spawn should all be batched together, so they should all // receive the same lane. - if (includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)) { + + // Check the priority of the current render to decide the priority of the + // deferred task. + + // OffscreenLane is used for prerendering, but we also use OffscreenLane + // for incremental hydration. It's given the lowest priority because the + // initial HTML is the same as the final UI. But useDeferredValue during + // hydration is an exception — we need to upgrade the UI to the final + // value. So if we're currently hydrating, we treat it like a transition. + const isPrerendering = + includesSomeLane(workInProgressRootRenderLanes, OffscreenLane) && + !getIsHydrating(); + if (isPrerendering) { // There's only one OffscreenLane, so if it contains deferred work, we // should just reschedule using the same lane. - // TODO: We also use OffscreenLane for hydration, on the basis that the - // initial HTML is the same as the hydrated UI, but since the deferred - // task will change the UI, it should be treated like an update. Use - // TransitionHydrationLane to trigger selective hydration. workInProgressDeferredLane = OffscreenLane; } else { // Everything else is spawned as a transition.