@samitouri / QOS-React-1 / commits / be67db46b6

Add optional initialValue argument to useDeferredValue (#27500)

Adds a second argument to useDeferredValue called initialValue: ```js const value = useDeferredValue(finalValue, initialValue); ``` During the initial render of a component, useDeferredValue will return initialValue. Once that render finishes, it will spawn an additional render to switch to finalValue. This same sequence should occur whenever the hook is hidden and revealed again, i.e. by a Suspense or Activity, though this part is not yet implemented. When initialValue is not provided, useDeferredValue has no effect during initial render, but during an update, it will remain on the previous value, then spawn an additional render to switch to the new value. (This is the same behavior that exists today.) During SSR, initialValue is always used, if provided. This feature is currently behind an experimental flag. We plan to ship it in a non-breaking release.

Andrew Clark committed Oct 10, 2023 at 16:39 UTC be67db46b60d94f9fbefccf2523429af25873e5b
17 files changed +189 -36
packages/react-debug-tools/src/ReactDebugHooks.js
+1 -1
@@ -306,7 +306,7 @@ function useTransition(): [
306 return [false, callback => {}];
307 }
308
309 -function useDeferredValue<T>(value: T): T {
309 +function useDeferredValue<T>(value: T, initialValue?: T): T {
310 const hook = nextHook();
311 hookLog.push({
312 primitive: 'DeferredValue',
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1 -3
@@ -573,9 +573,7 @@ describe('ReactHooksInspectionIntegration', () => {
573
574 it('should support useDeferredValue hook', () => {
575 function Foo(props) {
576 - React.useDeferredValue('abc', {
577 - timeoutMs: 500,
578 - });
576 + React.useDeferredValue('abc');
577 const memoizedValue = React.useMemo(() => 1, []);
578 React.useMemo(() => 2, []);
579 return <div>{memoizedValue}</div>;
packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js new
+71
@@ -0,0 +1,71 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
13 +
14 +// Polyfills for test environment
15 +global.ReadableStream =
16 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17 +global.TextEncoder = require('util').TextEncoder;
18 +
19 +let act;
20 +let container;
21 +let React;
22 +let ReactDOMServer;
23 +let ReactDOMClient;
24 +let useDeferredValue;
25 +
26 +describe('ReactDOMFizzForm', () => {
27 + beforeEach(() => {
28 + jest.resetModules();
29 + React = require('react');
30 + ReactDOMServer = require('react-dom/server.browser');
31 + ReactDOMClient = require('react-dom/client');
32 + useDeferredValue = require('react').useDeferredValue;
33 + act = require('internal-test-utils').act;
34 + container = document.createElement('div');
35 + document.body.appendChild(container);
36 + });
37 +
38 + afterEach(() => {
39 + document.body.removeChild(container);
40 + });
41 +
42 + async function readIntoContainer(stream) {
43 + const reader = stream.getReader();
44 + let result = '';
45 + while (true) {
46 + const {done, value} = await reader.read();
47 + if (done) {
48 + break;
49 + }
50 + result += Buffer.from(value).toString('utf8');
51 + }
52 + const temp = document.createElement('div');
53 + temp.innerHTML = result;
54 + insertNodesAndExecuteScripts(temp, container, null);
55 + }
56 +
57 + // @gate enableUseDeferredValueInitialArg
58 + it('returns initialValue argument, if provided', async () => {
59 + function App() {
60 + return useDeferredValue('Final', 'Initial');
61 + }
62 +
63 + const stream = await ReactDOMServer.renderToReadableStream(<App />);
64 + await readIntoContainer(stream);
65 + expect(container.textContent).toEqual('Initial');
66 +
67 + // After hydration, it's updated to the final value
68 + await act(() => ReactDOMClient.hydrateRoot(container, <App />));
69 + expect(container.textContent).toEqual('Final');
70 + });
71 +});
packages/react-reconciler/src/ReactFiberHooks.js
+61 -24
@@ -41,6 +41,7 @@ import {
41 debugRenderPhaseSideEffectsForStrictMode,
42 enableAsyncActions,
43 enableFormActions,
44 + enableUseDeferredValueInitialArg,
45 } from 'shared/ReactFeatureFlags';
46 import {
47 REACT_CONTEXT_TYPE,
@@ -2638,33 +2639,69 @@ function updateMemo<T>(
2639 return nextValue;
2640 }
2641
2641 -function mountDeferredValue<T>(value: T): T {
2642 +function mountDeferredValue<T>(value: T, initialValue?: T): T {
2643 const hook = mountWorkInProgressHook();
2643 - hook.memoizedState = value;
2644 - return value;
2644 + return mountDeferredValueImpl(hook, value, initialValue);
2645 }
2646
2647 -function updateDeferredValue<T>(value: T): T {
2647 +function updateDeferredValue<T>(value: T, initialValue?: T): T {
2648 const hook = updateWorkInProgressHook();
2649 const resolvedCurrentHook: Hook = (currentHook: any);
2650 const prevValue: T = resolvedCurrentHook.memoizedState;
2651 - return updateDeferredValueImpl(hook, prevValue, value);
2651 + return updateDeferredValueImpl(hook, prevValue, value, initialValue);
2652 }
2653
2654 -function rerenderDeferredValue<T>(value: T): T {
2654 +function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
2655 const hook = updateWorkInProgressHook();
2656 if (currentHook === null) {
2657 // This is a rerender during a mount.
2658 - hook.memoizedState = value;
2659 - return value;
2658 + return mountDeferredValueImpl(hook, value, initialValue);
2659 } else {
2660 // This is a rerender during an update.
2661 const prevValue: T = currentHook.memoizedState;
2663 - return updateDeferredValueImpl(hook, prevValue, value);
2662 + return updateDeferredValueImpl(hook, prevValue, value, initialValue);
2663 }
2664 }
2665
2667 -function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
2666 +function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
2667 + if (enableUseDeferredValueInitialArg && initialValue !== undefined) {
2668 + // When `initialValue` is provided, we defer the initial render even if the
2669 + // current render is not synchronous.
2670 + // TODO: However, to avoid waterfalls, we should not defer if this render
2671 + // was itself spawned by an earlier useDeferredValue. Plan is to add a
2672 + // Deferred lane to track this.
2673 + hook.memoizedState = initialValue;
2674 +
2675 + // Schedule a deferred render
2676 + const deferredLane = claimNextTransitionLane();
2677 + currentlyRenderingFiber.lanes = mergeLanes(
2678 + currentlyRenderingFiber.lanes,
2679 + deferredLane,
2680 + );
2681 + markSkippedUpdateLanes(deferredLane);
2682 +
2683 + // Set this to true to indicate that the rendered value is inconsistent
2684 + // from the latest value. The name "baseState" doesn't really match how we
2685 + // use it because we're reusing a state hook field instead of creating a
2686 + // new one.
2687 + hook.baseState = true;
2688 +
2689 + return initialValue;
2690 + } else {
2691 + hook.memoizedState = value;
2692 + return value;
2693 + }
2694 +}
2695 +
2696 +function updateDeferredValueImpl<T>(
2697 + hook: Hook,
2698 + prevValue: T,
2699 + value: T,
2700 + initialValue: ?T,
2701 +): T {
2702 + // TODO: We should also check if this component is going from
2703 + // hidden -> visible. If so, it should use the initialValue arg.
2704 +
2705 const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
2706 if (shouldDeferValue) {
2707 // This is an urgent update. If the value has changed, keep using the
@@ -3633,10 +3670,10 @@ if (__DEV__) {
3670 mountHookTypesDev();
3671 return mountDebugValue(value, formatterFn);
3672 },
3636 - useDeferredValue<T>(value: T): T {
3673 + useDeferredValue<T>(value: T, initialValue?: T): T {
3674 currentHookNameInDev = 'useDeferredValue';
3675 mountHookTypesDev();
3639 - return mountDeferredValue(value);
3676 + return mountDeferredValue(value, initialValue);
3677 },
3678 useTransition(): [boolean, (() => void) => void] {
3679 currentHookNameInDev = 'useTransition';
@@ -3802,10 +3839,10 @@ if (__DEV__) {
3839 updateHookTypesDev();
3840 return mountDebugValue(value, formatterFn);
3841 },
3805 - useDeferredValue<T>(value: T): T {
3842 + useDeferredValue<T>(value: T, initialValue?: T): T {
3843 currentHookNameInDev = 'useDeferredValue';
3844 updateHookTypesDev();
3808 - return mountDeferredValue(value);
3845 + return mountDeferredValue(value, initialValue);
3846 },
3847 useTransition(): [boolean, (() => void) => void] {
3848 currentHookNameInDev = 'useTransition';
@@ -3975,10 +4012,10 @@ if (__DEV__) {
4012 updateHookTypesDev();
4013 return updateDebugValue(value, formatterFn);
4014 },
3978 - useDeferredValue<T>(value: T): T {
4015 + useDeferredValue<T>(value: T, initialValue?: T): T {
4016 currentHookNameInDev = 'useDeferredValue';
4017 updateHookTypesDev();
3981 - return updateDeferredValue(value);
4018 + return updateDeferredValue(value, initialValue);
4019 },
4020 useTransition(): [boolean, (() => void) => void] {
4021 currentHookNameInDev = 'useTransition';
@@ -4147,10 +4184,10 @@ if (__DEV__) {
4184 updateHookTypesDev();
4185 return updateDebugValue(value, formatterFn);
4186 },
4150 - useDeferredValue<T>(value: T): T {
4187 + useDeferredValue<T>(value: T, initialValue?: T): T {
4188 currentHookNameInDev = 'useDeferredValue';
4189 updateHookTypesDev();
4153 - return rerenderDeferredValue(value);
4190 + return rerenderDeferredValue(value, initialValue);
4191 },
4192 useTransition(): [boolean, (() => void) => void] {
4193 currentHookNameInDev = 'useTransition';
@@ -4331,11 +4368,11 @@ if (__DEV__) {
4368 mountHookTypesDev();
4369 return mountDebugValue(value, formatterFn);
4370 },
4334 - useDeferredValue<T>(value: T): T {
4371 + useDeferredValue<T>(value: T, initialValue?: T): T {
4372 currentHookNameInDev = 'useDeferredValue';
4373 warnInvalidHookAccess();
4374 mountHookTypesDev();
4338 - return mountDeferredValue(value);
4375 + return mountDeferredValue(value, initialValue);
4376 },
4377 useTransition(): [boolean, (() => void) => void] {
4378 currentHookNameInDev = 'useTransition';
@@ -4529,11 +4566,11 @@ if (__DEV__) {
4566 updateHookTypesDev();
4567 return updateDebugValue(value, formatterFn);
4568 },
4532 - useDeferredValue<T>(value: T): T {
4569 + useDeferredValue<T>(value: T, initialValue?: T): T {
4570 currentHookNameInDev = 'useDeferredValue';
4571 warnInvalidHookAccess();
4572 updateHookTypesDev();
4536 - return updateDeferredValue(value);
4573 + return updateDeferredValue(value, initialValue);
4574 },
4575 useTransition(): [boolean, (() => void) => void] {
4576 currentHookNameInDev = 'useTransition';
@@ -4727,11 +4764,11 @@ if (__DEV__) {
4764 updateHookTypesDev();
4765 return updateDebugValue(value, formatterFn);
4766 },
4730 - useDeferredValue<T>(value: T): T {
4767 + useDeferredValue<T>(value: T, initialValue?: T): T {
4768 currentHookNameInDev = 'useDeferredValue';
4769 warnInvalidHookAccess();
4770 updateHookTypesDev();
4734 - return rerenderDeferredValue(value);
4771 + return rerenderDeferredValue(value, initialValue);
4772 },
4773 useTransition(): [boolean, (() => void) => void] {
4774 currentHookNameInDev = 'useTransition';
packages/react-reconciler/src/ReactInternalTypes.js
+1 -1
@@ -399,7 +399,7 @@ export type Dispatcher = {
399 deps: Array<mixed> | void | null,
400 ): void,
401 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
402 - useDeferredValue<T>(value: T): T,
402 + useDeferredValue<T>(value: T, initialValue?: T): T,
403 useTransition(): [
404 boolean,
405 (callback: () => void, options?: StartTransitionOptions) => void,
packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+35
@@ -306,4 +306,39 @@ describe('ReactDeferredValue', () => {
306 );
307 });
308 });
309 +
310 + // @gate enableUseDeferredValueInitialArg
311 + it('supports initialValue argument', async () => {
312 + function App() {
313 + const value = useDeferredValue('Final', 'Initial');
314 + return <Text text={value} />;
315 + }
316 +
317 + const root = ReactNoop.createRoot();
318 + await act(async () => {
319 + root.render(<App />);
320 + await waitForPaint(['Initial']);
321 + expect(root).toMatchRenderedOutput('Initial');
322 + });
323 + assertLog(['Final']);
324 + expect(root).toMatchRenderedOutput('Final');
325 + });
326 +
327 + // @gate enableUseDeferredValueInitialArg
328 + it('defers during initial render when initialValue is provided, even if render is not sync', async () => {
329 + function App() {
330 + const value = useDeferredValue('Final', 'Initial');
331 + return <Text text={value} />;
332 + }
333 +
334 + const root = ReactNoop.createRoot();
335 + await act(async () => {
336 + // Initial mount is a transition, but it should defer anyway
337 + startTransition(() => root.render(<App />));
338 + await waitForPaint(['Initial']);
339 + expect(root).toMatchRenderedOutput('Initial');
340 + });
341 + assertLog(['Final']);
342 + expect(root).toMatchRenderedOutput('Final');
343 + });
344 });
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+1 -3
@@ -3584,9 +3584,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3584 let _setText;
3585 function App() {
3586 const [text, setText] = useState('A');
3587 - const deferredText = useDeferredValue(text, {
3588 - timeoutMs: 500,
3589 - });
3587 + const deferredText = useDeferredValue(text);
3588 _setText = setText;
3589 return (
3590 <>
packages/react-server/src/ReactFizzHooks.js
+7 -2
@@ -35,6 +35,7 @@ import {
35 enableUseMemoCacheHook,
36 enableAsyncActions,
37 enableFormActions,
38 + enableUseDeferredValueInitialArg,
39 } from 'shared/ReactFeatureFlags';
40 import is from 'shared/objectIs';
41 import {
@@ -553,9 +554,13 @@ function useSyncExternalStore<T>(
554 return getServerSnapshot();
555 }
556
556 -function useDeferredValue<T>(value: T): T {
557 +function useDeferredValue<T>(value: T, initialValue?: T): T {
558 resolveCurrentlyRenderingComponent();
558 - return value;
559 + if (enableUseDeferredValueInitialArg) {
560 + return initialValue !== undefined ? initialValue : value;
561 + } else {
562 + return value;
563 + }
564 }
565
566 function unsupportedStartTransition() {
packages/react/src/ReactHooks.js
+2 -2
@@ -181,9 +181,9 @@ export function useTransition(): [
181 return dispatcher.useTransition();
182 }
183
184 -export function useDeferredValue<T>(value: T): T {
184 +export function useDeferredValue<T>(value: T, initialValue?: T): T {
185 const dispatcher = resolveDispatcher();
186 - return dispatcher.useDeferredValue(value);
186 + return dispatcher.useDeferredValue(value, initialValue);
187 }
188
189 export function useId(): string {
packages/shared/ReactFeatureFlags.js
+2
@@ -126,6 +126,8 @@ export const useMicrotasksForSchedulingInFabric = false;
126
127 export const passChildrenWhenCloningPersistedNodes = false;
128
129 +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
130 +
131 // -----------------------------------------------------------------------------
132 // Chopping Block
133 //
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -89,6 +89,7 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
89 export const enableFizzExternalRuntime = false;
90
91 export const enableAsyncActions = false;
92 +export const enableUseDeferredValueInitialArg = true;
93
94 // Flow magic to verify the exports of this file match the original version.
95 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
80
81 export const useMicrotasksForSchedulingInFabric = false;
82 export const passChildrenWhenCloningPersistedNodes = false;
83 +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
84
85 // Flow magic to verify the exports of this file match the original version.
86 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
80
81 export const useMicrotasksForSchedulingInFabric = false;
82 export const passChildrenWhenCloningPersistedNodes = false;
83 +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
84
85 // Flow magic to verify the exports of this file match the original version.
86 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -77,6 +77,7 @@ export const alwaysThrottleRetries = true;
77
78 export const useMicrotasksForSchedulingInFabric = false;
79 export const passChildrenWhenCloningPersistedNodes = false;
80 +export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
81
82 // Flow magic to verify the exports of this file match the original version.
83 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
80
81 export const useMicrotasksForSchedulingInFabric = false;
82 export const passChildrenWhenCloningPersistedNodes = false;
83 +export const enableUseDeferredValueInitialArg = true;
84
85 // Flow magic to verify the exports of this file match the original version.
86 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -28,6 +28,7 @@ export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
28 export const enableAsyncActions = __VARIANT__;
29 export const alwaysThrottleRetries = __VARIANT__;
30 export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
31 +export const enableUseDeferredValueInitialArg = __VARIANT__;
32
33 // Enable this flag to help with concurrent mode debugging.
34 // It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -31,6 +31,7 @@ export const {
31 alwaysThrottleRetries,
32 enableDO_NOT_USE_disableStrictPassiveEffect,
33 disableSchedulerTimeoutInWorkLoop,
34 + enableUseDeferredValueInitialArg,
35 } = dynamicFeatureFlags;
36
37 // On WWW, __EXPERIMENTAL__ is used for a new modern build.