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

Add optional initialValue argument to useDeferredValue #27500

Merged
merged 2 commits into from
Oct 10, 2023
Merged
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 @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
71 changes: 71 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
85 changes: 61 additions & 24 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
debugRenderPhaseSideEffectsForStrictMode,
enableAsyncActions,
enableFormActions,
enableUseDeferredValueInitialArg,
} from 'shared/ReactFeatureFlags';
import {
REACT_CONTEXT_TYPE,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactInternalTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand Down
9 changes: 7 additions & 2 deletions packages/react-server/src/ReactFizzHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
enableUseMemoCacheHook,
enableAsyncActions,
enableFormActions,
enableUseDeferredValueInitialArg,
} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';
import {
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/ReactHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export const useMicrotasksForSchedulingInFabric = false;

export const passChildrenWhenCloningPersistedNodes = false;

export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;

// -----------------------------------------------------------------------------
// Chopping Block
//
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Loading