Skip to content

Commit a800512

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 render will always use this value and spawn a deferred render to switch to the canonical value. Unlike with updates, the initial render will be deferered regardless of the render priority. This makes it suitable for progressive enhancement. When initialValue is omitted, the behavior is the same as today.
1 parent 8bc527a commit a800512

File tree

9 files changed

+194
-50
lines changed

9 files changed

+194
-50
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__/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: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,32 +1927,54 @@ 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 a mount, 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.
1958+
if (initialValue !== undefined && !is(value, initialValue)) {
1959+
// Spawn a deferred render
1960+
const deferredLane = claimNextTransitionLane();
1961+
currentlyRenderingFiber.lanes = mergeLanes(
1962+
currentlyRenderingFiber.lanes,
1963+
deferredLane,
1964+
);
1965+
markSkippedUpdateLanes(deferredLane);
1966+
1967+
// Set this to true to indicate that the rendered value is inconsistent
1968+
// from the latest value. The name "baseState" doesn't really match how we
1969+
// use it because we're reusing a state hook field instead of creating a
1970+
// new one.
1971+
hook.baseState = true;
1972+
value = initialValue;
1973+
}
1974+
hook.memoizedState = value;
1975+
return value;
1976+
}
1977+
19561978
function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
19571979
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
19581980
if (shouldDeferValue) {
@@ -1993,6 +2015,7 @@ function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
19932015
markWorkInProgressReceivedUpdate();
19942016
}
19952017

2018+
hook.memoizedState = value;
19962019
return value;
19972020
}
19982021
}
@@ -2663,10 +2686,10 @@ if (__DEV__) {
26632686
mountHookTypesDev();
26642687
return mountDebugValue(value, formatterFn);
26652688
},
2666-
useDeferredValue<T>(value: T): T {
2689+
useDeferredValue<T>(value: T, initialValue?: T): T {
26672690
currentHookNameInDev = 'useDeferredValue';
26682691
mountHookTypesDev();
2669-
return mountDeferredValue(value);
2692+
return mountDeferredValue(value, initialValue);
26702693
},
26712694
useTransition(): [boolean, (() => void) => void] {
26722695
currentHookNameInDev = 'useTransition';
@@ -2805,10 +2828,10 @@ if (__DEV__) {
28052828
updateHookTypesDev();
28062829
return mountDebugValue(value, formatterFn);
28072830
},
2808-
useDeferredValue<T>(value: T): T {
2831+
useDeferredValue<T>(value: T, initialValue?: T): T {
28092832
currentHookNameInDev = 'useDeferredValue';
28102833
updateHookTypesDev();
2811-
return mountDeferredValue(value);
2834+
return mountDeferredValue(value, initialValue);
28122835
},
28132836
useTransition(): [boolean, (() => void) => void] {
28142837
currentHookNameInDev = 'useTransition';
@@ -2947,10 +2970,10 @@ if (__DEV__) {
29472970
updateHookTypesDev();
29482971
return updateDebugValue(value, formatterFn);
29492972
},
2950-
useDeferredValue<T>(value: T): T {
2973+
useDeferredValue<T>(value: T, initialValue?: T): T {
29512974
currentHookNameInDev = 'useDeferredValue';
29522975
updateHookTypesDev();
2953-
return updateDeferredValue(value);
2976+
return updateDeferredValue(value, initialValue);
29542977
},
29552978
useTransition(): [boolean, (() => void) => void] {
29562979
currentHookNameInDev = 'useTransition';
@@ -3090,10 +3113,10 @@ if (__DEV__) {
30903113
updateHookTypesDev();
30913114
return updateDebugValue(value, formatterFn);
30923115
},
3093-
useDeferredValue<T>(value: T): T {
3116+
useDeferredValue<T>(value: T, initialValue?: T): T {
30943117
currentHookNameInDev = 'useDeferredValue';
30953118
updateHookTypesDev();
3096-
return rerenderDeferredValue(value);
3119+
return rerenderDeferredValue(value, initialValue);
30973120
},
30983121
useTransition(): [boolean, (() => void) => void] {
30993122
currentHookNameInDev = 'useTransition';
@@ -3244,11 +3267,11 @@ if (__DEV__) {
32443267
mountHookTypesDev();
32453268
return mountDebugValue(value, formatterFn);
32463269
},
3247-
useDeferredValue<T>(value: T): T {
3270+
useDeferredValue<T>(value: T, initialValue?: T): T {
32483271
currentHookNameInDev = 'useDeferredValue';
32493272
warnInvalidHookAccess();
32503273
mountHookTypesDev();
3251-
return mountDeferredValue(value);
3274+
return mountDeferredValue(value, initialValue);
32523275
},
32533276
useTransition(): [boolean, (() => void) => void] {
32543277
currentHookNameInDev = 'useTransition';
@@ -3403,11 +3426,11 @@ if (__DEV__) {
34033426
updateHookTypesDev();
34043427
return updateDebugValue(value, formatterFn);
34053428
},
3406-
useDeferredValue<T>(value: T): T {
3429+
useDeferredValue<T>(value: T, initialValue?: T): T {
34073430
currentHookNameInDev = 'useDeferredValue';
34083431
warnInvalidHookAccess();
34093432
updateHookTypesDev();
3410-
return updateDeferredValue(value);
3433+
return updateDeferredValue(value, initialValue);
34113434
},
34123435
useTransition(): [boolean, (() => void) => void] {
34133436
currentHookNameInDev = 'useTransition';
@@ -3563,11 +3586,11 @@ if (__DEV__) {
35633586
updateHookTypesDev();
35643587
return updateDebugValue(value, formatterFn);
35653588
},
3566-
useDeferredValue<T>(value: T): T {
3589+
useDeferredValue<T>(value: T, initialValue?: T): T {
35673590
currentHookNameInDev = 'useDeferredValue';
35683591
warnInvalidHookAccess();
35693592
updateHookTypesDev();
3570-
return rerenderDeferredValue(value);
3593+
return rerenderDeferredValue(value, initialValue);
35713594
},
35723595
useTransition(): [boolean, (() => void) => void] {
35733596
currentHookNameInDev = 'useTransition';

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

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,32 +1927,54 @@ 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 a mount, 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.
1958+
if (initialValue !== undefined && !is(value, initialValue)) {
1959+
// Spawn a deferred render
1960+
const deferredLane = claimNextTransitionLane();
1961+
currentlyRenderingFiber.lanes = mergeLanes(
1962+
currentlyRenderingFiber.lanes,
1963+
deferredLane,
1964+
);
1965+
markSkippedUpdateLanes(deferredLane);
1966+
1967+
// Set this to true to indicate that the rendered value is inconsistent
1968+
// from the latest value. The name "baseState" doesn't really match how we
1969+
// use it because we're reusing a state hook field instead of creating a
1970+
// new one.
1971+
hook.baseState = true;
1972+
value = initialValue;
1973+
}
1974+
hook.memoizedState = value;
1975+
return value;
1976+
}
1977+
19561978
function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
19571979
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
19581980
if (shouldDeferValue) {
@@ -1993,6 +2015,7 @@ function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
19932015
markWorkInProgressReceivedUpdate();
19942016
}
19952017

2018+
hook.memoizedState = value;
19962019
return value;
19972020
}
19982021
}
@@ -2663,10 +2686,10 @@ if (__DEV__) {
26632686
mountHookTypesDev();
26642687
return mountDebugValue(value, formatterFn);
26652688
},
2666-
useDeferredValue<T>(value: T): T {
2689+
useDeferredValue<T>(value: T, initialValue?: T): T {
26672690
currentHookNameInDev = 'useDeferredValue';
26682691
mountHookTypesDev();
2669-
return mountDeferredValue(value);
2692+
return mountDeferredValue(value, initialValue);
26702693
},
26712694
useTransition(): [boolean, (() => void) => void] {
26722695
currentHookNameInDev = 'useTransition';
@@ -2805,10 +2828,10 @@ if (__DEV__) {
28052828
updateHookTypesDev();
28062829
return mountDebugValue(value, formatterFn);
28072830
},
2808-
useDeferredValue<T>(value: T): T {
2831+
useDeferredValue<T>(value: T, initialValue?: T): T {
28092832
currentHookNameInDev = 'useDeferredValue';
28102833
updateHookTypesDev();
2811-
return mountDeferredValue(value);
2834+
return mountDeferredValue(value, initialValue);
28122835
},
28132836
useTransition(): [boolean, (() => void) => void] {
28142837
currentHookNameInDev = 'useTransition';
@@ -2947,10 +2970,10 @@ if (__DEV__) {
29472970
updateHookTypesDev();
29482971
return updateDebugValue(value, formatterFn);
29492972
},
2950-
useDeferredValue<T>(value: T): T {
2973+
useDeferredValue<T>(value: T, initialValue?: T): T {
29512974
currentHookNameInDev = 'useDeferredValue';
29522975
updateHookTypesDev();
2953-
return updateDeferredValue(value);
2976+
return updateDeferredValue(value, initialValue);
29542977
},
29552978
useTransition(): [boolean, (() => void) => void] {
29562979
currentHookNameInDev = 'useTransition';
@@ -3090,10 +3113,10 @@ if (__DEV__) {
30903113
updateHookTypesDev();
30913114
return updateDebugValue(value, formatterFn);
30923115
},
3093-
useDeferredValue<T>(value: T): T {
3116+
useDeferredValue<T>(value: T, initialValue?: T): T {
30943117
currentHookNameInDev = 'useDeferredValue';
30953118
updateHookTypesDev();
3096-
return rerenderDeferredValue(value);
3119+
return rerenderDeferredValue(value, initialValue);
30973120
},
30983121
useTransition(): [boolean, (() => void) => void] {
30993122
currentHookNameInDev = 'useTransition';
@@ -3244,11 +3267,11 @@ if (__DEV__) {
32443267
mountHookTypesDev();
32453268
return mountDebugValue(value, formatterFn);
32463269
},
3247-
useDeferredValue<T>(value: T): T {
3270+
useDeferredValue<T>(value: T, initialValue?: T): T {
32483271
currentHookNameInDev = 'useDeferredValue';
32493272
warnInvalidHookAccess();
32503273
mountHookTypesDev();
3251-
return mountDeferredValue(value);
3274+
return mountDeferredValue(value, initialValue);
32523275
},
32533276
useTransition(): [boolean, (() => void) => void] {
32543277
currentHookNameInDev = 'useTransition';
@@ -3403,11 +3426,11 @@ if (__DEV__) {
34033426
updateHookTypesDev();
34043427
return updateDebugValue(value, formatterFn);
34053428
},
3406-
useDeferredValue<T>(value: T): T {
3429+
useDeferredValue<T>(value: T, initialValue?: T): T {
34073430
currentHookNameInDev = 'useDeferredValue';
34083431
warnInvalidHookAccess();
34093432
updateHookTypesDev();
3410-
return updateDeferredValue(value);
3433+
return updateDeferredValue(value, initialValue);
34113434
},
34123435
useTransition(): [boolean, (() => void) => void] {
34133436
currentHookNameInDev = 'useTransition';
@@ -3563,11 +3586,11 @@ if (__DEV__) {
35633586
updateHookTypesDev();
35643587
return updateDebugValue(value, formatterFn);
35653588
},
3566-
useDeferredValue<T>(value: T): T {
3589+
useDeferredValue<T>(value: T, initialValue?: T): T {
35673590
currentHookNameInDev = 'useDeferredValue';
35683591
warnInvalidHookAccess();
35693592
updateHookTypesDev();
3570-
return rerenderDeferredValue(value);
3593+
return rerenderDeferredValue(value, initialValue);
35713594
},
35723595
useTransition(): [boolean, (() => void) => void] {
35733596
currentHookNameInDev = 'useTransition';

0 commit comments

Comments
 (0)