Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Add initialValue option to useDeferredValue #24369

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
38 changes: 38 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let Suspense;
let SuspenseList;
let useSyncExternalStore;
let useSyncExternalStoreWithSelector;
let useDeferredValue;
let PropTypes;
let textCache;
let window;
Expand Down Expand Up @@ -61,6 +62,7 @@ describe('ReactDOMFizzServer', () => {
useSyncExternalStore = React.useSyncExternalStore;
useSyncExternalStoreWithSelector = require('use-sync-external-store/with-selector')
.useSyncExternalStoreWithSelector;
useDeferredValue = React.useDeferredValue;

textCache = new Map();

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

expect(Scheduler).toFlushAndYield([]);
});

// @gate experimental
it('useDeferredValue uses initialValue during hydration even if render is not urgent', async () => {
function Child() {
const value = useDeferredValue('Canonical', 'Initial');
Scheduler.unstable_yieldValue(value);
return value;
}

function App() {
// Because the child is wrapped in a Suspense boundary, it hydrates
// at a non-urgent priority.
return (
<Suspense fallback="Loading...">
<Child />
</Suspense>
);
}

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(Scheduler).toHaveYielded(['Initial']);
// The server always renders the initial value, not the canonical one.
expect(getVisibleChildren(container)).toEqual('Initial');

// When hydrating, it should use the initial value even if the hydration
// is (per usual) not urgent, to avoid a hydration mismatch. Then it does
// a deferred client render to switch to the canonical value.
// TODO: The deferred render should not be higher than the hydration itself,
// but currently it's always a transition.
ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushAndYield(['Initial', 'Canonical']);
expect(getVisibleChildren(container)).toEqual('Canonical');
});
});
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
69 changes: 48 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,59 @@ 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 hydration, 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. Otherwise, we only use the initial value if the
// render is urgent — same logic as during an update.
if (
(getIsHydrating() || !includesOnlyNonUrgentLanes(renderLanes)) &&
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 @@ -2664,10 +2691,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 @@ -2806,10 +2833,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 @@ -2948,10 +2975,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 @@ -3091,10 +3118,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 @@ -3245,11 +3272,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 @@ -3404,11 +3431,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 @@ -3564,11 +3591,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