diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 6c8acd1e2f92d..2d75ff127e7bb 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -306,7 +306,7 @@ function useTransition(): [ return [false, callback => {}]; } -function useDeferredValue(value: T): T { +function useDeferredValue(value: T, initialValue?: T): T { const hook = nextHook(); hookLog.push({ primitive: 'DeferredValue', 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 ae4b68b592d5c..e0f5d268bcf57 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -41,6 +41,7 @@ import { debugRenderPhaseSideEffectsForStrictMode, enableAsyncActions, enableFormActions, + enableUseDeferredValueInitialArg, } from 'shared/ReactFeatureFlags'; import { REACT_CONTEXT_TYPE, @@ -2638,33 +2639,69 @@ function updateMemo( return nextValue; } -function mountDeferredValue(value: T): T { +function mountDeferredValue(value: T, initialValue?: T): T { const hook = mountWorkInProgressHook(); - hook.memoizedState = value; - return value; + return mountDeferredValueImpl(hook, value, initialValue); } -function updateDeferredValue(value: T): T { +function updateDeferredValue(value: T, initialValue?: T): T { const hook = updateWorkInProgressHook(); const resolvedCurrentHook: Hook = (currentHook: any); const prevValue: T = resolvedCurrentHook.memoizedState; - return updateDeferredValueImpl(hook, prevValue, value); + return updateDeferredValueImpl(hook, prevValue, value, initialValue); } -function rerenderDeferredValue(value: T): T { +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; - return updateDeferredValueImpl(hook, prevValue, value); + return updateDeferredValueImpl(hook, prevValue, value, initialValue); } } -function updateDeferredValueImpl(hook: Hook, prevValue: T, value: T): T { +function mountDeferredValueImpl(hook: Hook, value: T, initialValue?: T): T { + if (enableUseDeferredValueInitialArg && 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 @@ -3633,10 +3670,10 @@ if (__DEV__) { mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; mountHookTypesDev(); - return mountDeferredValue(value); + return mountDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; @@ -3802,10 +3839,10 @@ if (__DEV__) { updateHookTypesDev(); return mountDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; updateHookTypesDev(); - return mountDeferredValue(value); + return mountDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; @@ -3975,10 +4012,10 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; updateHookTypesDev(); - return updateDeferredValue(value); + return updateDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; @@ -4147,10 +4184,10 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; updateHookTypesDev(); - return rerenderDeferredValue(value); + return rerenderDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; @@ -4331,11 +4368,11 @@ if (__DEV__) { mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; warnInvalidHookAccess(); mountHookTypesDev(); - return mountDeferredValue(value); + return mountDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; @@ -4529,11 +4566,11 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; warnInvalidHookAccess(); updateHookTypesDev(); - return updateDeferredValue(value); + return updateDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; @@ -4727,11 +4764,11 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, - useDeferredValue(value: T): T { + useDeferredValue(value: T, initialValue?: T): T { currentHookNameInDev = 'useDeferredValue'; warnInvalidHookAccess(); updateHookTypesDev(); - return rerenderDeferredValue(value); + return rerenderDeferredValue(value, initialValue); }, useTransition(): [boolean, (() => void) => void] { currentHookNameInDev = 'useTransition'; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index b603a9fec3328..9378a41477e0f 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -399,7 +399,7 @@ export type Dispatcher = { deps: Array | void | null, ): void, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void, - useDeferredValue(value: T): T, + useDeferredValue(value: T, initialValue?: T): T, useTransition(): [ boolean, (callback: () => void, options?: StartTransitionOptions) => void, 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 c80ac1e3b621a..63d1327b85963 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -35,6 +35,7 @@ import { enableUseMemoCacheHook, enableAsyncActions, enableFormActions, + enableUseDeferredValueInitialArg, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; import { @@ -553,9 +554,13 @@ function useSyncExternalStore( return getServerSnapshot(); } -function useDeferredValue(value: T): T { +function useDeferredValue(value: T, initialValue?: T): T { resolveCurrentlyRenderingComponent(); - return value; + if (enableUseDeferredValueInitialArg) { + return initialValue !== undefined ? initialValue : value; + } else { + return value; + } } function unsupportedStartTransition() { diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 112baab1eaa5f..a2141cbc5dfbe 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -181,9 +181,9 @@ export function useTransition(): [ return dispatcher.useTransition(); } -export function useDeferredValue(value: T): T { +export function useDeferredValue(value: T, initialValue?: T): T { const dispatcher = resolveDispatcher(); - return dispatcher.useDeferredValue(value); + return dispatcher.useDeferredValue(value, initialValue); } export function useId(): string { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 5ca0e6e291fcd..751028ab29768 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -126,6 +126,8 @@ export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; + // ----------------------------------------------------------------------------- // Chopping Block // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 99d7392719256..c2aa5f9323332 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -89,6 +89,7 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const enableFizzExternalRuntime = false; export const enableAsyncActions = false; +export const enableUseDeferredValueInitialArg = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index ee0b2d91d387c..912d1b685100d 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 5d541fa3948ef..70ea8e5204662 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 23a44293eeb68..6ec32f3ef5341 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -77,6 +77,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 121442de9660d..60b2373dcf498 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; +export const enableUseDeferredValueInitialArg = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index db3adeb56d770..1fb76bf4b31de 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -28,6 +28,7 @@ export const enableDeferRootSchedulingToMicrotask = __VARIANT__; export const enableAsyncActions = __VARIANT__; export const alwaysThrottleRetries = __VARIANT__; export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__; +export const enableUseDeferredValueInitialArg = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index a004364fa4ab4..f3f036911d9a5 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -31,6 +31,7 @@ export const { alwaysThrottleRetries, enableDO_NOT_USE_disableStrictPassiveEffect, disableSchedulerTimeoutInWorkLoop, + enableUseDeferredValueInitialArg, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build.