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() {