Skip to content

Commit 9eab191

Browse files
committed
Implement useDeferredValue initialValue option
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. 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 0917467 commit 9eab191

File tree

6 files changed

+144
-11
lines changed

6 files changed

+144
-11
lines changed

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: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,8 +2640,7 @@ function updateMemo<T>(
26402640

26412641
function mountDeferredValue<T>(value: T, initialValue?: T): T {
26422642
const hook = mountWorkInProgressHook();
2643-
hook.memoizedState = value;
2644-
return value;
2643+
return mountDeferredValueImpl(hook, value, initialValue);
26452644
}
26462645

26472646
function updateDeferredValue<T>(value: T, initialValue?: T): T {
@@ -2655,21 +2654,53 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
26552654
const hook = updateWorkInProgressHook();
26562655
if (currentHook === null) {
26572656
// This is a rerender during a mount.
2658-
hook.memoizedState = value;
2659-
return value;
2657+
return mountDeferredValueImpl(hook, value, initialValue);
26602658
} else {
26612659
// This is a rerender during an update.
26622660
const prevValue: T = currentHook.memoizedState;
26632661
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
26642662
}
26652663
}
26662664

2665+
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
2666+
if (initialValue !== undefined) {
2667+
// When `initialValue` is provided, we defer the initial render even if the
2668+
// current render is not synchronous.
2669+
// TODO: However, to avoid waterfalls, we should not defer if this render
2670+
// was itself spawned by an earlier useDeferredValue. Plan is to add a
2671+
// Deferred lane to track this.
2672+
hook.memoizedState = initialValue;
2673+
2674+
// Schedule a deferred render
2675+
const deferredLane = claimNextTransitionLane();
2676+
currentlyRenderingFiber.lanes = mergeLanes(
2677+
currentlyRenderingFiber.lanes,
2678+
deferredLane,
2679+
);
2680+
markSkippedUpdateLanes(deferredLane);
2681+
2682+
// Set this to true to indicate that the rendered value is inconsistent
2683+
// from the latest value. The name "baseState" doesn't really match how we
2684+
// use it because we're reusing a state hook field instead of creating a
2685+
// new one.
2686+
hook.baseState = true;
2687+
2688+
return initialValue;
2689+
} else {
2690+
hook.memoizedState = value;
2691+
return value;
2692+
}
2693+
}
2694+
26672695
function updateDeferredValueImpl<T>(
26682696
hook: Hook,
26692697
prevValue: T,
26702698
value: T,
26712699
initialValue: ?T,
26722700
): T {
2701+
// TODO: We should also check if this component is going from
2702+
// hidden -> visible. If so, it should use the initialValue arg.
2703+
26732704
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
26742705
if (shouldDeferValue) {
26752706
// This is an urgent update. If the value has changed, keep using the

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ function useSyncExternalStore<T>(
555555

556556
function useDeferredValue<T>(value: T, initialValue?: T): T {
557557
resolveCurrentlyRenderingComponent();
558-
return value;
558+
return initialValue !== undefined ? initialValue : value;
559559
}
560560

561561
function unsupportedStartTransition() {

0 commit comments

Comments
 (0)