mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add optional initialValue argument to useDeferredValue (#27500)
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. (This is the same behavior that exists today.) 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.
This commit is contained in:
+1
-1
@@ -306,7 +306,7 @@ function useTransition(): [
|
||||
return [false, callback => {}];
|
||||
}
|
||||
|
||||
function useDeferredValue<T>(value: T): T {
|
||||
function useDeferredValue<T>(value: T, initialValue?: T): T {
|
||||
const hook = nextHook();
|
||||
hookLog.push({
|
||||
primitive: 'DeferredValue',
|
||||
|
||||
+1
-3
@@ -573,9 +573,7 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||
|
||||
it('should support useDeferredValue hook', () => {
|
||||
function Foo(props) {
|
||||
React.useDeferredValue('abc', {
|
||||
timeoutMs: 500,
|
||||
});
|
||||
React.useDeferredValue('abc');
|
||||
const memoizedValue = React.useMemo(() => 1, []);
|
||||
React.useMemo(() => 2, []);
|
||||
return <div>{memoizedValue}</div>;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
|
||||
let act;
|
||||
let container;
|
||||
let React;
|
||||
let ReactDOMServer;
|
||||
let ReactDOMClient;
|
||||
let useDeferredValue;
|
||||
|
||||
describe('ReactDOMFizzForm', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOMServer = require('react-dom/server.browser');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
useDeferredValue = require('react').useDeferredValue;
|
||||
act = require('internal-test-utils').act;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
async function readIntoContainer(stream) {
|
||||
const reader = stream.getReader();
|
||||
let result = '';
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
result += Buffer.from(value).toString('utf8');
|
||||
}
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = result;
|
||||
insertNodesAndExecuteScripts(temp, container, null);
|
||||
}
|
||||
|
||||
// @gate enableUseDeferredValueInitialArg
|
||||
it('returns initialValue argument, if provided', async () => {
|
||||
function App() {
|
||||
return useDeferredValue('Final', 'Initial');
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toEqual('Initial');
|
||||
|
||||
// After hydration, it's updated to the final value
|
||||
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
|
||||
expect(container.textContent).toEqual('Final');
|
||||
});
|
||||
});
|
||||
+61
-24
@@ -41,6 +41,7 @@ import {
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
enableAsyncActions,
|
||||
enableFormActions,
|
||||
enableUseDeferredValueInitialArg,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
REACT_CONTEXT_TYPE,
|
||||
@@ -2638,33 +2639,69 @@ 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);
|
||||
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
|
||||
}
|
||||
|
||||
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);
|
||||
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
|
||||
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
|
||||
if (enableUseDeferredValueInitialArg && initialValue !== undefined) {
|
||||
// When `initialValue` is provided, we defer the initial render even if the
|
||||
// current render is not synchronous.
|
||||
// TODO: However, to avoid waterfalls, we should not defer if this render
|
||||
// was itself spawned by an earlier useDeferredValue. Plan is to add a
|
||||
// Deferred lane to track this.
|
||||
hook.memoizedState = initialValue;
|
||||
|
||||
// Schedule 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;
|
||||
|
||||
return initialValue;
|
||||
} else {
|
||||
hook.memoizedState = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDeferredValueImpl<T>(
|
||||
hook: Hook,
|
||||
prevValue: T,
|
||||
value: T,
|
||||
initialValue: ?T,
|
||||
): T {
|
||||
// TODO: We should also check if this component is going from
|
||||
// hidden -> visible. If so, it should use the initialValue arg.
|
||||
|
||||
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
|
||||
if (shouldDeferValue) {
|
||||
// This is an urgent update. If the value has changed, keep using the
|
||||
@@ -3633,10 +3670,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';
|
||||
@@ -3802,10 +3839,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';
|
||||
@@ -3975,10 +4012,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';
|
||||
@@ -4147,10 +4184,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';
|
||||
@@ -4331,11 +4368,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';
|
||||
@@ -4529,11 +4566,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';
|
||||
@@ -4727,11 +4764,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';
|
||||
|
||||
+1
-1
@@ -399,7 +399,7 @@ export type Dispatcher = {
|
||||
deps: Array<mixed> | void | null,
|
||||
): void,
|
||||
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
|
||||
useDeferredValue<T>(value: T): T,
|
||||
useDeferredValue<T>(value: T, initialValue?: T): T,
|
||||
useTransition(): [
|
||||
boolean,
|
||||
(callback: () => void, options?: StartTransitionOptions) => void,
|
||||
|
||||
@@ -306,4 +306,39 @@ describe('ReactDeferredValue', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @gate enableUseDeferredValueInitialArg
|
||||
it('supports initialValue argument', async () => {
|
||||
function App() {
|
||||
const value = useDeferredValue('Final', 'Initial');
|
||||
return <Text text={value} />;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
await waitForPaint(['Initial']);
|
||||
expect(root).toMatchRenderedOutput('Initial');
|
||||
});
|
||||
assertLog(['Final']);
|
||||
expect(root).toMatchRenderedOutput('Final');
|
||||
});
|
||||
|
||||
// @gate enableUseDeferredValueInitialArg
|
||||
it('defers during initial render when initialValue is provided, even if render is not sync', async () => {
|
||||
function App() {
|
||||
const value = useDeferredValue('Final', 'Initial');
|
||||
return <Text text={value} />;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
// Initial mount is a transition, but it should defer anyway
|
||||
startTransition(() => root.render(<App />));
|
||||
await waitForPaint(['Initial']);
|
||||
expect(root).toMatchRenderedOutput('Initial');
|
||||
});
|
||||
assertLog(['Final']);
|
||||
expect(root).toMatchRenderedOutput('Final');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3584,9 +3584,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
let _setText;
|
||||
function App() {
|
||||
const [text, setText] = useState('A');
|
||||
const deferredText = useDeferredValue(text, {
|
||||
timeoutMs: 500,
|
||||
});
|
||||
const deferredText = useDeferredValue(text);
|
||||
_setText = setText;
|
||||
return (
|
||||
<>
|
||||
|
||||
+7
-2
@@ -35,6 +35,7 @@ import {
|
||||
enableUseMemoCacheHook,
|
||||
enableAsyncActions,
|
||||
enableFormActions,
|
||||
enableUseDeferredValueInitialArg,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import is from 'shared/objectIs';
|
||||
import {
|
||||
@@ -553,9 +554,13 @@ function useSyncExternalStore<T>(
|
||||
return getServerSnapshot();
|
||||
}
|
||||
|
||||
function useDeferredValue<T>(value: T): T {
|
||||
function useDeferredValue<T>(value: T, initialValue?: T): T {
|
||||
resolveCurrentlyRenderingComponent();
|
||||
return value;
|
||||
if (enableUseDeferredValueInitialArg) {
|
||||
return initialValue !== undefined ? initialValue : value;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function unsupportedStartTransition() {
|
||||
|
||||
@@ -181,9 +181,9 @@ export function useTransition(): [
|
||||
return dispatcher.useTransition();
|
||||
}
|
||||
|
||||
export function useDeferredValue<T>(value: T): T {
|
||||
export function useDeferredValue<T>(value: T, initialValue?: T): T {
|
||||
const dispatcher = resolveDispatcher();
|
||||
return dispatcher.useDeferredValue(value);
|
||||
return dispatcher.useDeferredValue(value, initialValue);
|
||||
}
|
||||
|
||||
export function useId(): string {
|
||||
|
||||
@@ -126,6 +126,8 @@ export const useMicrotasksForSchedulingInFabric = false;
|
||||
|
||||
export const passChildrenWhenCloningPersistedNodes = false;
|
||||
|
||||
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Chopping Block
|
||||
//
|
||||
|
||||
@@ -89,6 +89,7 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
|
||||
export const enableFizzExternalRuntime = false;
|
||||
|
||||
export const enableAsyncActions = false;
|
||||
export const enableUseDeferredValueInitialArg = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
|
||||
|
||||
export const useMicrotasksForSchedulingInFabric = false;
|
||||
export const passChildrenWhenCloningPersistedNodes = false;
|
||||
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
|
||||
|
||||
export const useMicrotasksForSchedulingInFabric = false;
|
||||
export const passChildrenWhenCloningPersistedNodes = false;
|
||||
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -77,6 +77,7 @@ export const alwaysThrottleRetries = true;
|
||||
|
||||
export const useMicrotasksForSchedulingInFabric = false;
|
||||
export const passChildrenWhenCloningPersistedNodes = false;
|
||||
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -80,6 +80,7 @@ export const alwaysThrottleRetries = true;
|
||||
|
||||
export const useMicrotasksForSchedulingInFabric = false;
|
||||
export const passChildrenWhenCloningPersistedNodes = false;
|
||||
export const enableUseDeferredValueInitialArg = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -28,6 +28,7 @@ export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
|
||||
export const enableAsyncActions = __VARIANT__;
|
||||
export const alwaysThrottleRetries = __VARIANT__;
|
||||
export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
|
||||
export const enableUseDeferredValueInitialArg = __VARIANT__;
|
||||
|
||||
// Enable this flag to help with concurrent mode debugging.
|
||||
// It logs information to the console about React scheduling, rendering, and commit phases.
|
||||
|
||||
@@ -31,6 +31,7 @@ export const {
|
||||
alwaysThrottleRetries,
|
||||
enableDO_NOT_USE_disableStrictPassiveEffect,
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableUseDeferredValueInitialArg,
|
||||
} = dynamicFeatureFlags;
|
||||
|
||||
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
|
||||
|
||||
Reference in New Issue
Block a user