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.