diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
index 7283f3af7216c..3d5cd38f82eba 100644
--- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
@@ -573,9 +573,7 @@ describe('ReactHooksInspectionIntegration', () => {
it('should support useDeferredValue hook', () => {
function Foo(props) {
- React.useDeferredValue('abc', {
- timeoutMs: 500,
- });
+ React.useDeferredValue('abc');
const memoizedValue = React.useMemo(() => 1, []);
React.useMemo(() => 2, []);
return
{memoizedValue}
;
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js
new file mode 100644
index 0000000000000..4f8a4d98d654d
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
+
+// Polyfills for test environment
+global.ReadableStream =
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+
+let act;
+let container;
+let React;
+let ReactDOMServer;
+let ReactDOMClient;
+let useDeferredValue;
+
+describe('ReactDOMFizzForm', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOMServer = require('react-dom/server.browser');
+ ReactDOMClient = require('react-dom/client');
+ useDeferredValue = require('react').useDeferredValue;
+ act = require('internal-test-utils').act;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ async function readIntoContainer(stream) {
+ const reader = stream.getReader();
+ let result = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ result += Buffer.from(value).toString('utf8');
+ }
+ const temp = document.createElement('div');
+ temp.innerHTML = result;
+ insertNodesAndExecuteScripts(temp, container, null);
+ }
+
+ // @gate enableUseDeferredValueInitialArg
+ it('returns initialValue argument, if provided', async () => {
+ function App() {
+ return useDeferredValue('Final', 'Initial');
+ }
+
+ const stream = await ReactDOMServer.renderToReadableStream();
+ await readIntoContainer(stream);
+ expect(container.textContent).toEqual('Initial');
+
+ // After hydration, it's updated to the final value
+ await act(() => ReactDOMClient.hydrateRoot(container, ));
+ expect(container.textContent).toEqual('Final');
+ });
+});
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index 8f4f2492855c0..5878f1cf06cf7 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -2640,8 +2640,7 @@ function updateMemo(
function mountDeferredValue(value: T, initialValue?: T): T {
const hook = mountWorkInProgressHook();
- hook.memoizedState = value;
- return value;
+ return mountDeferredValueImpl(hook, value, initialValue);
}
function updateDeferredValue(value: T, initialValue?: T): T {
@@ -2655,8 +2654,7 @@ function rerenderDeferredValue(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
if (currentHook === null) {
// This is a rerender during a mount.
- hook.memoizedState = value;
- return value;
+ return mountDeferredValueImpl(hook, value, initialValue);
} else {
// This is a rerender during an update.
const prevValue: T = currentHook.memoizedState;
@@ -2664,12 +2662,45 @@ function rerenderDeferredValue(value: T, initialValue?: T): T {
}
}
+function mountDeferredValueImpl(hook: Hook, value: T, initialValue?: T): T {
+ if (initialValue !== undefined) {
+ // When `initialValue` is provided, we defer the initial render even if the
+ // current render is not synchronous.
+ // TODO: However, to avoid waterfalls, we should not defer if this render
+ // was itself spawned by an earlier useDeferredValue. Plan is to add a
+ // Deferred lane to track this.
+ hook.memoizedState = initialValue;
+
+ // Schedule a deferred render
+ const deferredLane = claimNextTransitionLane();
+ currentlyRenderingFiber.lanes = mergeLanes(
+ currentlyRenderingFiber.lanes,
+ deferredLane,
+ );
+ markSkippedUpdateLanes(deferredLane);
+
+ // Set this to true to indicate that the rendered value is inconsistent
+ // from the latest value. The name "baseState" doesn't really match how we
+ // use it because we're reusing a state hook field instead of creating a
+ // new one.
+ hook.baseState = true;
+
+ return initialValue;
+ } else {
+ hook.memoizedState = value;
+ return value;
+ }
+}
+
function updateDeferredValueImpl(
hook: Hook,
prevValue: T,
value: T,
initialValue: ?T,
): T {
+ // TODO: We should also check if this component is going from
+ // hidden -> visible. If so, it should use the initialValue arg.
+
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
if (shouldDeferValue) {
// This is an urgent update. If the value has changed, keep using the
diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
index 54f4ad42f44bc..c29a9c4287275 100644
--- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
@@ -306,4 +306,39 @@ describe('ReactDeferredValue', () => {
);
});
});
+
+ // @gate enableUseDeferredValueInitialArg
+ it('supports initialValue argument', async () => {
+ function App() {
+ const value = useDeferredValue('Final', 'Initial');
+ return ;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render();
+ await waitForPaint(['Initial']);
+ expect(root).toMatchRenderedOutput('Initial');
+ });
+ assertLog(['Final']);
+ expect(root).toMatchRenderedOutput('Final');
+ });
+
+ // @gate enableUseDeferredValueInitialArg
+ it('defers during initial render when initialValue is provided, even if render is not sync', async () => {
+ function App() {
+ const value = useDeferredValue('Final', 'Initial');
+ return ;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ // Initial mount is a transition, but it should defer anyway
+ startTransition(() => root.render());
+ await waitForPaint(['Initial']);
+ expect(root).toMatchRenderedOutput('Initial');
+ });
+ assertLog(['Final']);
+ expect(root).toMatchRenderedOutput('Final');
+ });
});
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
index db67dde0d7e1b..89d150ed09888 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
@@ -3584,9 +3584,7 @@ describe('ReactHooksWithNoopRenderer', () => {
let _setText;
function App() {
const [text, setText] = useState('A');
- const deferredText = useDeferredValue(text, {
- timeoutMs: 500,
- });
+ const deferredText = useDeferredValue(text);
_setText = setText;
return (
<>
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 3ecf1baa50a77..7581ab611d05a 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -555,7 +555,7 @@ function useSyncExternalStore(
function useDeferredValue(value: T, initialValue?: T): T {
resolveCurrentlyRenderingComponent();
- return value;
+ return initialValue !== undefined ? initialValue : value;
}
function unsupportedStartTransition() {