Skip to content

Commit 50340b2

Browse files
acdlitealunyov
authored andcommitted
Add optional initialValue argument to useDeferredValue (facebook#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.
1 parent 6cb0e12 commit 50340b2

17 files changed

+189
-36
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ function useTransition(): [
306306
return [false, callback => {}];
307307
}
308308

309-
function useDeferredValue<T>(value: T): T {
309+
function useDeferredValue<T>(value: T, initialValue?: T): T {
310310
const hook = nextHook();
311311
hookLog.push({
312312
primitive: 'DeferredValue',

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,7 @@ describe('ReactHooksInspectionIntegration', () => {
573573

574574
it('should support useDeferredValue hook', () => {
575575
function Foo(props) {
576-
React.useDeferredValue('abc', {
577-
timeoutMs: 500,
578-
});
576+
React.useDeferredValue('abc');
579577
const memoizedValue = React.useMemo(() => 1, []);
580578
React.useMemo(() => 2, []);
581579
return <div>{memoizedValue}</div>;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
debugRenderPhaseSideEffectsForStrictMode,
4242
enableAsyncActions,
4343
enableFormActions,
44+
enableUseDeferredValueInitialArg,
4445
} from 'shared/ReactFeatureFlags';
4546
import {
4647
REACT_CONTEXT_TYPE,
@@ -2638,33 +2639,69 @@ function updateMemo<T>(
26382639
return nextValue;
26392640
}
26402641

2641-
function mountDeferredValue<T>(value: T): T {
2642+
function mountDeferredValue<T>(value: T, initialValue?: T): T {
26422643
const hook = mountWorkInProgressHook();
2643-
hook.memoizedState = value;
2644-
return value;
2644+
return mountDeferredValueImpl(hook, value, initialValue);
26452645
}
26462646

2647-
function updateDeferredValue<T>(value: T): T {
2647+
function updateDeferredValue<T>(value: T, initialValue?: T): T {
26482648
const hook = updateWorkInProgressHook();
26492649
const resolvedCurrentHook: Hook = (currentHook: any);
26502650
const prevValue: T = resolvedCurrentHook.memoizedState;
2651-
return updateDeferredValueImpl(hook, prevValue, value);
2651+
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
26522652
}
26532653

2654-
function rerenderDeferredValue<T>(value: T): T {
2654+
function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
26552655
const hook = updateWorkInProgressHook();
26562656
if (currentHook === null) {
26572657
// This is a rerender during a mount.
2658-
hook.memoizedState = value;
2659-
return value;
2658+
return mountDeferredValueImpl(hook, value, initialValue);
26602659
} else {
26612660
// This is a rerender during an update.
26622661
const prevValue: T = currentHook.memoizedState;
2663-
return updateDeferredValueImpl(hook, prevValue, value);
2662+
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
26642663
}
26652664
}
26662665

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+
26682705
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
26692706
if (shouldDeferValue) {
26702707
// This is an urgent update. If the value has changed, keep using the
@@ -3633,10 +3670,10 @@ if (__DEV__) {
36333670
mountHookTypesDev();
36343671
return mountDebugValue(value, formatterFn);
36353672
},
3636-
useDeferredValue<T>(value: T): T {
3673+
useDeferredValue<T>(value: T, initialValue?: T): T {
36373674
currentHookNameInDev = 'useDeferredValue';
36383675
mountHookTypesDev();
3639-
return mountDeferredValue(value);
3676+
return mountDeferredValue(value, initialValue);
36403677
},
36413678
useTransition(): [boolean, (() => void) => void] {
36423679
currentHookNameInDev = 'useTransition';
@@ -3802,10 +3839,10 @@ if (__DEV__) {
38023839
updateHookTypesDev();
38033840
return mountDebugValue(value, formatterFn);
38043841
},
3805-
useDeferredValue<T>(value: T): T {
3842+
useDeferredValue<T>(value: T, initialValue?: T): T {
38063843
currentHookNameInDev = 'useDeferredValue';
38073844
updateHookTypesDev();
3808-
return mountDeferredValue(value);
3845+
return mountDeferredValue(value, initialValue);
38093846
},
38103847
useTransition(): [boolean, (() => void) => void] {
38113848
currentHookNameInDev = 'useTransition';
@@ -3975,10 +4012,10 @@ if (__DEV__) {
39754012
updateHookTypesDev();
39764013
return updateDebugValue(value, formatterFn);
39774014
},
3978-
useDeferredValue<T>(value: T): T {
4015+
useDeferredValue<T>(value: T, initialValue?: T): T {
39794016
currentHookNameInDev = 'useDeferredValue';
39804017
updateHookTypesDev();
3981-
return updateDeferredValue(value);
4018+
return updateDeferredValue(value, initialValue);
39824019
},
39834020
useTransition(): [boolean, (() => void) => void] {
39844021
currentHookNameInDev = 'useTransition';
@@ -4147,10 +4184,10 @@ if (__DEV__) {
41474184
updateHookTypesDev();
41484185
return updateDebugValue(value, formatterFn);
41494186
},
4150-
useDeferredValue<T>(value: T): T {
4187+
useDeferredValue<T>(value: T, initialValue?: T): T {
41514188
currentHookNameInDev = 'useDeferredValue';
41524189
updateHookTypesDev();
4153-
return rerenderDeferredValue(value);
4190+
return rerenderDeferredValue(value, initialValue);
41544191
},
41554192
useTransition(): [boolean, (() => void) => void] {
41564193
currentHookNameInDev = 'useTransition';
@@ -4331,11 +4368,11 @@ if (__DEV__) {
43314368
mountHookTypesDev();
43324369
return mountDebugValue(value, formatterFn);
43334370
},
4334-
useDeferredValue<T>(value: T): T {
4371+
useDeferredValue<T>(value: T, initialValue?: T): T {
43354372
currentHookNameInDev = 'useDeferredValue';
43364373
warnInvalidHookAccess();
43374374
mountHookTypesDev();
4338-
return mountDeferredValue(value);
4375+
return mountDeferredValue(value, initialValue);
43394376
},
43404377
useTransition(): [boolean, (() => void) => void] {
43414378
currentHookNameInDev = 'useTransition';
@@ -4529,11 +4566,11 @@ if (__DEV__) {
45294566
updateHookTypesDev();
45304567
return updateDebugValue(value, formatterFn);
45314568
},
4532-
useDeferredValue<T>(value: T): T {
4569+
useDeferredValue<T>(value: T, initialValue?: T): T {
45334570
currentHookNameInDev = 'useDeferredValue';
45344571
warnInvalidHookAccess();
45354572
updateHookTypesDev();
4536-
return updateDeferredValue(value);
4573+
return updateDeferredValue(value, initialValue);
45374574
},
45384575
useTransition(): [boolean, (() => void) => void] {
45394576
currentHookNameInDev = 'useTransition';
@@ -4727,11 +4764,11 @@ if (__DEV__) {
47274764
updateHookTypesDev();
47284765
return updateDebugValue(value, formatterFn);
47294766
},
4730-
useDeferredValue<T>(value: T): T {
4767+
useDeferredValue<T>(value: T, initialValue?: T): T {
47314768
currentHookNameInDev = 'useDeferredValue';
47324769
warnInvalidHookAccess();
47334770
updateHookTypesDev();
4734-
return rerenderDeferredValue(value);
4771+
return rerenderDeferredValue(value, initialValue);
47354772
},
47364773
useTransition(): [boolean, (() => void) => void] {
47374774
currentHookNameInDev = 'useTransition';

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ export type Dispatcher = {
399399
deps: Array<mixed> | void | null,
400400
): void,
401401
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
402-
useDeferredValue<T>(value: T): T,
402+
useDeferredValue<T>(value: T, initialValue?: T): T,
403403
useTransition(): [
404404
boolean,
405405
(callback: () => void, options?: StartTransitionOptions) => void,

packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,39 @@ describe('ReactDeferredValue', () => {
306306
);
307307
});
308308
});
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+
});
309344
});

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3584,9 +3584,7 @@ describe('ReactHooksWithNoopRenderer', () => {
35843584
let _setText;
35853585
function App() {
35863586
const [text, setText] = useState('A');
3587-
const deferredText = useDeferredValue(text, {
3588-
timeoutMs: 500,
3589-
});
3587+
const deferredText = useDeferredValue(text);
35903588
_setText = setText;
35913589
return (
35923590
<>

packages/react-server/src/ReactFizzHooks.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
enableUseMemoCacheHook,
3636
enableAsyncActions,
3737
enableFormActions,
38+
enableUseDeferredValueInitialArg,
3839
} from 'shared/ReactFeatureFlags';
3940
import is from 'shared/objectIs';
4041
import {
@@ -553,9 +554,13 @@ function useSyncExternalStore<T>(
553554
return getServerSnapshot();
554555
}
555556

556-
function useDeferredValue<T>(value: T): T {
557+
function useDeferredValue<T>(value: T, initialValue?: T): T {
557558
resolveCurrentlyRenderingComponent();
558-
return value;
559+
if (enableUseDeferredValueInitialArg) {
560+
return initialValue !== undefined ? initialValue : value;
561+
} else {
562+
return value;
563+
}
559564
}
560565

561566
function unsupportedStartTransition() {

packages/react/src/ReactHooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ export function useTransition(): [
181181
return dispatcher.useTransition();
182182
}
183183

184-
export function useDeferredValue<T>(value: T): T {
184+
export function useDeferredValue<T>(value: T, initialValue?: T): T {
185185
const dispatcher = resolveDispatcher();
186-
return dispatcher.useDeferredValue(value);
186+
return dispatcher.useDeferredValue(value, initialValue);
187187
}
188188

189189
export function useId(): string {

packages/shared/ReactFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export const useMicrotasksForSchedulingInFabric = false;
126126

127127
export const passChildrenWhenCloningPersistedNodes = false;
128128

129+
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
130+
129131
// -----------------------------------------------------------------------------
130132
// Chopping Block
131133
//

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
8989
export const enableFizzExternalRuntime = false;
9090

9191
export const enableAsyncActions = false;
92+
export const enableUseDeferredValueInitialArg = true;
9293

9394
// Flow magic to verify the exports of this file match the original version.
9495
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
8080

8181
export const useMicrotasksForSchedulingInFabric = false;
8282
export const passChildrenWhenCloningPersistedNodes = false;
83+
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
8384

8485
// Flow magic to verify the exports of this file match the original version.
8586
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

0 commit comments

Comments
 (0)