Skip to content

Commit

Permalink
Add initialValue option to useDeferredValue
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Apr 13, 2022
1 parent 8bc527a commit 2f14b06
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 53 deletions.
2 changes: 1 addition & 1 deletion packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ function useTransition(): [
return [false, callback => {}];
}

function useDeferredValue<T>(value: T): T {
function useDeferredValue<T>(value: T, initialValue?: T): T {
// useDeferredValue() composes multiple hooks internally.
// Advance the current hook index the same number of times
// so that subsequent hooks have the right memoized state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let useImperativeHandle;
let useInsertionEffect;
let useLayoutEffect;
let useDebugValue;
let useDeferredValue;
let forwardRef;
let yieldedValues;
let yieldValue;
Expand All @@ -52,6 +53,7 @@ function initModules() {
useImperativeHandle = React.useImperativeHandle;
useInsertionEffect = React.useInsertionEffect;
useLayoutEffect = React.useLayoutEffect;
useDeferredValue = React.useDeferredValue;
forwardRef = React.forwardRef;

yieldedValues = [];
Expand Down Expand Up @@ -663,6 +665,32 @@ describe('ReactDOMServerHooks', () => {
});
});

describe('useDeferredValue', () => {
it('renders with initialValue, if provided', async () => {
function Counter() {
const value1 = useDeferredValue('Latest', 'Initial');
const value2 = useDeferredValue('Latest');
return (
<div>
<div>{value1}</div>
<div>{value2}</div>
</div>
);
}
const domNode = await serverRender(<Counter />, 1);
expect(domNode).toMatchInlineSnapshot(`
<div>
<div>
Initial
</div>
<div>
Latest
</div>
</div>
`);
});
});

describe('useContext', () => {
itThrowsWhenRendering(
'if used inside a class component',
Expand Down
4 changes: 2 additions & 2 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,9 @@ function useSyncExternalStore<T>(
return getServerSnapshot();
}

function useDeferredValue<T>(value: T): T {
function useDeferredValue<T>(value: T, initialValue?: T): T {
resolveCurrentlyRenderingComponent();
return value;
return initialValue !== undefined ? initialValue : value;
}

function useTransition(): [boolean, (callback: () => void) => void] {
Expand Down
65 changes: 44 additions & 21 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -1927,32 +1927,54 @@ function updateMemo<T>(
return nextValue;
}

function mountDeferredValue<T>(value: T): T {
function mountDeferredValue<T>(value: T, initialValue?: T): T {
const hook = mountWorkInProgressHook();
hook.memoizedState = value;
return value;
return mountDeferredValueImpl(hook, value, initialValue);
}

function updateDeferredValue<T>(value: T): T {
function updateDeferredValue<T>(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
const resolvedCurrentHook: Hook = (currentHook: any);
const prevValue: T = resolvedCurrentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}

function rerenderDeferredValue<T>(value: T): T {
function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
if (currentHook === null) {
// This is a rerender during a mount.
hook.memoizedState = value;
return value;
return mountDeferredValueImpl(hook, value, initialValue);
} else {
// This is a rerender during an update.
const prevValue: T = currentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}
}

function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
// During a mount, if an initial value is provided, we always use that one
// regardless of the render priority. This means you can use it for
// progressive enhancement.
if (initialValue !== undefined && !is(value, initialValue)) {
// Spawn a deferred render
const deferredLane = claimNextTransitionLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane,
);
markSkippedUpdateLanes(deferredLane);

// Set this to true to indicate that the rendered value is inconsistent
// from the latest value. The name "baseState" doesn't really match how we
// use it because we're reusing a state hook field instead of creating a
// new one.
hook.baseState = true;
value = initialValue;
}
hook.memoizedState = value;
return value;
}

function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
if (shouldDeferValue) {
Expand Down Expand Up @@ -1993,6 +2015,7 @@ function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
markWorkInProgressReceivedUpdate();
}

hook.memoizedState = value;
return value;
}
}
Expand Down Expand Up @@ -2663,10 +2686,10 @@ if (__DEV__) {
mountHookTypesDev();
return mountDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
mountHookTypesDev();
return mountDeferredValue(value);
return mountDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -2805,10 +2828,10 @@ if (__DEV__) {
updateHookTypesDev();
return mountDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
updateHookTypesDev();
return mountDeferredValue(value);
return mountDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -2947,10 +2970,10 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
updateHookTypesDev();
return updateDeferredValue(value);
return updateDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3090,10 +3113,10 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
updateHookTypesDev();
return rerenderDeferredValue(value);
return rerenderDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3244,11 +3267,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
warnInvalidHookAccess();
mountHookTypesDev();
return mountDeferredValue(value);
return mountDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3403,11 +3426,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
warnInvalidHookAccess();
updateHookTypesDev();
return updateDeferredValue(value);
return updateDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3563,11 +3586,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
warnInvalidHookAccess();
updateHookTypesDev();
return rerenderDeferredValue(value);
return rerenderDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down
65 changes: 44 additions & 21 deletions packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -1927,32 +1927,54 @@ function updateMemo<T>(
return nextValue;
}

function mountDeferredValue<T>(value: T): T {
function mountDeferredValue<T>(value: T, initialValue?: T): T {
const hook = mountWorkInProgressHook();
hook.memoizedState = value;
return value;
return mountDeferredValueImpl(hook, value, initialValue);
}

function updateDeferredValue<T>(value: T): T {
function updateDeferredValue<T>(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
const resolvedCurrentHook: Hook = (currentHook: any);
const prevValue: T = resolvedCurrentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}

function rerenderDeferredValue<T>(value: T): T {
function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
if (currentHook === null) {
// This is a rerender during a mount.
hook.memoizedState = value;
return value;
return mountDeferredValueImpl(hook, value, initialValue);
} else {
// This is a rerender during an update.
const prevValue: T = currentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}
}

function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
// During a mount, if an initial value is provided, we always use that one
// regardless of the render priority. This means you can use it for
// progressive enhancement.
if (initialValue !== undefined && !is(value, initialValue)) {
// Spawn a deferred render
const deferredLane = claimNextTransitionLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane,
);
markSkippedUpdateLanes(deferredLane);

// Set this to true to indicate that the rendered value is inconsistent
// from the latest value. The name "baseState" doesn't really match how we
// use it because we're reusing a state hook field instead of creating a
// new one.
hook.baseState = true;
value = initialValue;
}
hook.memoizedState = value;
return value;
}

function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
if (shouldDeferValue) {
Expand Down Expand Up @@ -1993,6 +2015,7 @@ function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
markWorkInProgressReceivedUpdate();
}

hook.memoizedState = value;
return value;
}
}
Expand Down Expand Up @@ -2663,10 +2686,10 @@ if (__DEV__) {
mountHookTypesDev();
return mountDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
mountHookTypesDev();
return mountDeferredValue(value);
return mountDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -2805,10 +2828,10 @@ if (__DEV__) {
updateHookTypesDev();
return mountDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
updateHookTypesDev();
return mountDeferredValue(value);
return mountDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -2947,10 +2970,10 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
updateHookTypesDev();
return updateDeferredValue(value);
return updateDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3090,10 +3113,10 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
updateHookTypesDev();
return rerenderDeferredValue(value);
return rerenderDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3244,11 +3267,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
warnInvalidHookAccess();
mountHookTypesDev();
return mountDeferredValue(value);
return mountDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3403,11 +3426,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
warnInvalidHookAccess();
updateHookTypesDev();
return updateDeferredValue(value);
return updateDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down Expand Up @@ -3563,11 +3586,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
useDeferredValue<T>(value: T): T {
useDeferredValue<T>(value: T, initialValue?: T): T {
currentHookNameInDev = 'useDeferredValue';
warnInvalidHookAccess();
updateHookTypesDev();
return rerenderDeferredValue(value);
return rerenderDeferredValue(value, initialValue);
},
useTransition(): [boolean, (() => void) => void] {
currentHookNameInDev = 'useTransition';
Expand Down
Loading

0 comments on commit 2f14b06

Please sign in to comment.