Skip to content

Commit b441be7

Browse files
committed
Add initialValue option to useDeferredValue
Currently, useDeferredValue only works for updates. It will never during the initial render because there's no previous value to reuse. This means it can't be used to implement progressive enhancement. This adds an optional initialValue argument to useDeferredValue. When provided, the initial mount will use initialValue if it's during an urgent render. Otherwise it will use the latest, canonical value. During server rendering and hydration, it will always use the initialValue instead of the canonical value, regardless of priority, to avoid a hydration mismatch. The name "initial value" isn't ideal because during a non-urgent client render, it's disregarded entirely. It's more like a "lightweight" value that will later be upgraded to a "heavier" one. Needs some bikeshedding. When initialValue is omitted, the behavior is the same as today.
1 parent bd08137 commit b441be7

11 files changed

+218
-53
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ function useTransition(): [
309309
return [false, callback => {}];
310310
}
311311

312-
function useDeferredValue<T>(value: T): T {
312+
function useDeferredValue<T>(value: T, initialValue?: T): T {
313313
// useDeferredValue() composes multiple hooks internally.
314314
// Advance the current hook index the same number of times
315315
// so that subsequent hooks have the right memoized state.

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ let Suspense;
1919
let SuspenseList;
2020
let useSyncExternalStore;
2121
let useSyncExternalStoreWithSelector;
22+
let useDeferredValue;
2223
let PropTypes;
2324
let textCache;
2425
let window;
@@ -61,6 +62,7 @@ describe('ReactDOMFizzServer', () => {
6162
useSyncExternalStore = React.useSyncExternalStore;
6263
useSyncExternalStoreWithSelector = require('use-sync-external-store/with-selector')
6364
.useSyncExternalStoreWithSelector;
65+
useDeferredValue = React.useDeferredValue;
6466

6567
textCache = new Map();
6668

@@ -3084,4 +3086,40 @@ describe('ReactDOMFizzServer', () => {
30843086

30853087
expect(Scheduler).toFlushAndYield([]);
30863088
});
3089+
3090+
// @gate experimental
3091+
it('useDeferredValue uses initialValue during hydration even if render is not urgent', async () => {
3092+
function Child() {
3093+
const value = useDeferredValue('Canonical', 'Initial');
3094+
Scheduler.unstable_yieldValue(value);
3095+
return value;
3096+
}
3097+
3098+
function App() {
3099+
// Because the child is wrapped in a Suspense boundary, it hydrates
3100+
// at a non-urgent priority.
3101+
return (
3102+
<Suspense fallback="Loading...">
3103+
<Child />
3104+
</Suspense>
3105+
);
3106+
}
3107+
3108+
await act(async () => {
3109+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3110+
pipe(writable);
3111+
});
3112+
expect(Scheduler).toHaveYielded(['Initial']);
3113+
// The server always renders the initial value, not the canonical one.
3114+
expect(getVisibleChildren(container)).toEqual('Initial');
3115+
3116+
// When hydrating, it should use the initial value even if the hydration
3117+
// is (per usual) not urgent, to avoid a hydration mismatch. Then it does
3118+
// a deferred client render to switch to the canonical value.
3119+
// TODO: The deferred render should not be higher than the hydration itself,
3120+
// but currently it's always a transition.
3121+
ReactDOMClient.hydrateRoot(container, <App />);
3122+
expect(Scheduler).toFlushAndYield(['Initial', 'Canonical']);
3123+
expect(getVisibleChildren(container)).toEqual('Canonical');
3124+
});
30873125
});

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let useImperativeHandle;
2828
let useInsertionEffect;
2929
let useLayoutEffect;
3030
let useDebugValue;
31+
let useDeferredValue;
3132
let forwardRef;
3233
let yieldedValues;
3334
let yieldValue;
@@ -52,6 +53,7 @@ function initModules() {
5253
useImperativeHandle = React.useImperativeHandle;
5354
useInsertionEffect = React.useInsertionEffect;
5455
useLayoutEffect = React.useLayoutEffect;
56+
useDeferredValue = React.useDeferredValue;
5557
forwardRef = React.forwardRef;
5658

5759
yieldedValues = [];
@@ -663,6 +665,32 @@ describe('ReactDOMServerHooks', () => {
663665
});
664666
});
665667

668+
describe('useDeferredValue', () => {
669+
it('renders with initialValue, if provided', async () => {
670+
function Counter() {
671+
const value1 = useDeferredValue('Latest', 'Initial');
672+
const value2 = useDeferredValue('Latest');
673+
return (
674+
<div>
675+
<div>{value1}</div>
676+
<div>{value2}</div>
677+
</div>
678+
);
679+
}
680+
const domNode = await serverRender(<Counter />, 1);
681+
expect(domNode).toMatchInlineSnapshot(`
682+
<div>
683+
<div>
684+
Initial
685+
</div>
686+
<div>
687+
Latest
688+
</div>
689+
</div>
690+
`);
691+
});
692+
});
693+
666694
describe('useContext', () => {
667695
itThrowsWhenRendering(
668696
'if used inside a class component',

packages/react-dom/src/server/ReactPartialRendererHooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,9 +496,9 @@ function useSyncExternalStore<T>(
496496
return getServerSnapshot();
497497
}
498498

499-
function useDeferredValue<T>(value: T): T {
499+
function useDeferredValue<T>(value: T, initialValue?: T): T {
500500
resolveCurrentlyRenderingComponent();
501-
return value;
501+
return initialValue !== undefined ? initialValue : value;
502502
}
503503

504504
function useTransition(): [boolean, (callback: () => void) => void] {

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,32 +1927,59 @@ function updateMemo<T>(
19271927
return nextValue;
19281928
}
19291929

1930-
function mountDeferredValue<T>(value: T): T {
1930+
function mountDeferredValue<T>(value: T, initialValue?: T): T {
19311931
const hook = mountWorkInProgressHook();
1932-
hook.memoizedState = value;
1933-
return value;
1932+
return mountDeferredValueImpl(hook, value, initialValue);
19341933
}
19351934

1936-
function updateDeferredValue<T>(value: T): T {
1935+
function updateDeferredValue<T>(value: T, initialValue?: T): T {
19371936
const hook = updateWorkInProgressHook();
19381937
const resolvedCurrentHook: Hook = (currentHook: any);
19391938
const prevValue: T = resolvedCurrentHook.memoizedState;
19401939
return updateDeferredValueImpl(hook, prevValue, value);
19411940
}
19421941

1943-
function rerenderDeferredValue<T>(value: T): T {
1942+
function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
19441943
const hook = updateWorkInProgressHook();
19451944
if (currentHook === null) {
19461945
// This is a rerender during a mount.
1947-
hook.memoizedState = value;
1948-
return value;
1946+
return mountDeferredValueImpl(hook, value, initialValue);
19491947
} else {
19501948
// This is a rerender during an update.
19511949
const prevValue: T = currentHook.memoizedState;
19521950
return updateDeferredValueImpl(hook, prevValue, value);
19531951
}
19541952
}
19551953

1954+
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
1955+
// During hydration, if an initial value is provided, we always use that one
1956+
// regardless of the render priority. This means you can use it for
1957+
// progressive enhancement. Otherwise, we only use the initial value if the
1958+
// render is urgent — same logic as during an update.
1959+
if (
1960+
(getIsHydrating() || !includesOnlyNonUrgentLanes(renderLanes)) &&
1961+
initialValue !== undefined &&
1962+
!is(value, initialValue)
1963+
) {
1964+
// Spawn a deferred render
1965+
const deferredLane = claimNextTransitionLane();
1966+
currentlyRenderingFiber.lanes = mergeLanes(
1967+
currentlyRenderingFiber.lanes,
1968+
deferredLane,
1969+
);
1970+
markSkippedUpdateLanes(deferredLane);
1971+
1972+
// Set this to true to indicate that the rendered value is inconsistent
1973+
// from the latest value. The name "baseState" doesn't really match how we
1974+
// use it because we're reusing a state hook field instead of creating a
1975+
// new one.
1976+
hook.baseState = true;
1977+
value = initialValue;
1978+
}
1979+
hook.memoizedState = value;
1980+
return value;
1981+
}
1982+
19561983
function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
19571984
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
19581985
if (shouldDeferValue) {
@@ -2664,10 +2691,10 @@ if (__DEV__) {
26642691
mountHookTypesDev();
26652692
return mountDebugValue(value, formatterFn);
26662693
},
2667-
useDeferredValue<T>(value: T): T {
2694+
useDeferredValue<T>(value: T, initialValue?: T): T {
26682695
currentHookNameInDev = 'useDeferredValue';
26692696
mountHookTypesDev();
2670-
return mountDeferredValue(value);
2697+
return mountDeferredValue(value, initialValue);
26712698
},
26722699
useTransition(): [boolean, (() => void) => void] {
26732700
currentHookNameInDev = 'useTransition';
@@ -2806,10 +2833,10 @@ if (__DEV__) {
28062833
updateHookTypesDev();
28072834
return mountDebugValue(value, formatterFn);
28082835
},
2809-
useDeferredValue<T>(value: T): T {
2836+
useDeferredValue<T>(value: T, initialValue?: T): T {
28102837
currentHookNameInDev = 'useDeferredValue';
28112838
updateHookTypesDev();
2812-
return mountDeferredValue(value);
2839+
return mountDeferredValue(value, initialValue);
28132840
},
28142841
useTransition(): [boolean, (() => void) => void] {
28152842
currentHookNameInDev = 'useTransition';
@@ -2948,10 +2975,10 @@ if (__DEV__) {
29482975
updateHookTypesDev();
29492976
return updateDebugValue(value, formatterFn);
29502977
},
2951-
useDeferredValue<T>(value: T): T {
2978+
useDeferredValue<T>(value: T, initialValue?: T): T {
29522979
currentHookNameInDev = 'useDeferredValue';
29532980
updateHookTypesDev();
2954-
return updateDeferredValue(value);
2981+
return updateDeferredValue(value, initialValue);
29552982
},
29562983
useTransition(): [boolean, (() => void) => void] {
29572984
currentHookNameInDev = 'useTransition';
@@ -3091,10 +3118,10 @@ if (__DEV__) {
30913118
updateHookTypesDev();
30923119
return updateDebugValue(value, formatterFn);
30933120
},
3094-
useDeferredValue<T>(value: T): T {
3121+
useDeferredValue<T>(value: T, initialValue?: T): T {
30953122
currentHookNameInDev = 'useDeferredValue';
30963123
updateHookTypesDev();
3097-
return rerenderDeferredValue(value);
3124+
return rerenderDeferredValue(value, initialValue);
30983125
},
30993126
useTransition(): [boolean, (() => void) => void] {
31003127
currentHookNameInDev = 'useTransition';
@@ -3245,11 +3272,11 @@ if (__DEV__) {
32453272
mountHookTypesDev();
32463273
return mountDebugValue(value, formatterFn);
32473274
},
3248-
useDeferredValue<T>(value: T): T {
3275+
useDeferredValue<T>(value: T, initialValue?: T): T {
32493276
currentHookNameInDev = 'useDeferredValue';
32503277
warnInvalidHookAccess();
32513278
mountHookTypesDev();
3252-
return mountDeferredValue(value);
3279+
return mountDeferredValue(value, initialValue);
32533280
},
32543281
useTransition(): [boolean, (() => void) => void] {
32553282
currentHookNameInDev = 'useTransition';
@@ -3404,11 +3431,11 @@ if (__DEV__) {
34043431
updateHookTypesDev();
34053432
return updateDebugValue(value, formatterFn);
34063433
},
3407-
useDeferredValue<T>(value: T): T {
3434+
useDeferredValue<T>(value: T, initialValue?: T): T {
34083435
currentHookNameInDev = 'useDeferredValue';
34093436
warnInvalidHookAccess();
34103437
updateHookTypesDev();
3411-
return updateDeferredValue(value);
3438+
return updateDeferredValue(value, initialValue);
34123439
},
34133440
useTransition(): [boolean, (() => void) => void] {
34143441
currentHookNameInDev = 'useTransition';
@@ -3564,11 +3591,11 @@ if (__DEV__) {
35643591
updateHookTypesDev();
35653592
return updateDebugValue(value, formatterFn);
35663593
},
3567-
useDeferredValue<T>(value: T): T {
3594+
useDeferredValue<T>(value: T, initialValue?: T): T {
35683595
currentHookNameInDev = 'useDeferredValue';
35693596
warnInvalidHookAccess();
35703597
updateHookTypesDev();
3571-
return rerenderDeferredValue(value);
3598+
return rerenderDeferredValue(value, initialValue);
35723599
},
35733600
useTransition(): [boolean, (() => void) => void] {
35743601
currentHookNameInDev = 'useTransition';

0 commit comments

Comments
 (0)