/*** 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.*/'use strict';
let React;
let ReactNoop;
let Scheduler;
let act;
let startTransition;
let useDeferredValue;
let useMemo;
let useState;
let Suspense;
let Activity;
let assertLog;
let waitForPaint;
let textCache;
describe('ReactDeferredValue', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
startTransition = React.startTransition;
useDeferredValue = React.useDeferredValue;
useMemo = React.useMemo;
useState = React.useState;
Suspense = React.Suspense;
Activity = React.unstable_Activity;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
textCache = new Map();
});function resolveText(text) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}}function readText(text) {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
Scheduler.log(`Suspend! [${text}]`);
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}} else {
Scheduler.log(`Suspend! [${text}]`);
const thenable = {
pings: [],then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}},};const newRecord = {
status: 'pending',
value: thenable,
};textCache.set(text, newRecord);
throw thenable;
}}function Text({text}) {
Scheduler.log(text);
return text;
}function AsyncText({text}) {
readText(text);
Scheduler.log(text);
return text;
}it('does not cause an infinite defer loop if the original value isn\t memoized', async () => {
function App({value}) {
// The object passed to useDeferredValue is never the same as the previous
// render. A naive implementation would endlessly spawn deferred renders.
const {value: deferredValue} = useDeferredValue({value});
const child = useMemo(
() => <Text text={'Original: ' + value} />,
[value],
);const deferredChild = useMemo(
() => <Text text={'Deferred: ' + deferredValue} />,
[deferredValue],
);return (
<div>
<div>{child}</div>
<div>{deferredChild}</div>
</div>
);}const root = ReactNoop.createRoot();
// Initial render
await act(() => {
root.render(<App value={1} />);
});assertLog(['Original: 1', 'Deferred: 1']);
// If it's an urgent update, the value is deferred
await act(async () => {
root.render(<App value={2} />);
await waitForPaint(['Original: 2']);
// The deferred value updates in a separate render
await waitForPaint(['Deferred: 2']);
});expect(root).toMatchRenderedOutput(
<div>
<div>Original: 2</div>
<div>Deferred: 2</div>
</div>,
);// But if it updates during a transition, it doesn't defer
await act(async () => {
startTransition(() => {
root.render(<App value={3} />);
});// The deferred value updates in the same render as the original
await waitForPaint(['Original: 3', 'Deferred: 3']);
});expect(root).toMatchRenderedOutput(
<div>
<div>Original: 3</div>
<div>Deferred: 3</div>
</div>,
);});it('does not defer during a transition', async () => {function App({value}) {const deferredValue = useDeferredValue(value);const child = useMemo(() => <Text text={'Original: ' + value} />,
[value],
);const deferredChild = useMemo(() => <Text text={'Deferred: ' + deferredValue} />,
[deferredValue],
);return (<div><div>{child}</div><div>{deferredChild}</div></div>);}const root = ReactNoop.createRoot();
// Initial renderawait act(() => {root.render(<App value={1} />);
});assertLog(['Original: 1', 'Deferred: 1']);
// If it's an urgent update, the value is deferredawait act(async () => {root.render(<App value={2} />);
await waitForPaint(['Original: 2']);
// The deferred value updates in a separate renderawait waitForPaint(['Deferred: 2']);
});expect(root).toMatchRenderedOutput(
<div><div>Original: 2</div><div>Deferred: 2</div></div>,);// But if it updates during a transition, it doesn't deferawait act(async () => {startTransition(() => {root.render(<App value={3} />);
});// The deferred value updates in the same render as the originalawait waitForPaint(['Original: 3', 'Deferred: 3']);
});expect(root).toMatchRenderedOutput(
<div><div>Original: 3</div><div>Deferred: 3</div></div>,);});it("works if there's a render phase update", async () => {function App({value: propValue}) {const [value, setValue] = useState(null);
if (value !== propValue) {setValue(propValue);}const deferredValue = useDeferredValue(value);const child = useMemo(() => <Text text={'Original: ' + value} />,
[value],
);const deferredChild = useMemo(() => <Text text={'Deferred: ' + deferredValue} />,
[deferredValue],
);return (<div><div>{child}</div><div>{deferredChild}</div></div>);}const root = ReactNoop.createRoot();
// Initial renderawait act(() => {root.render(<App value={1} />);
});assertLog(['Original: 1', 'Deferred: 1']);
// If it's an urgent update, the value is deferredawait act(async () => {root.render(<App value={2} />);
await waitForPaint(['Original: 2']);
// The deferred value updates in a separate renderawait waitForPaint(['Deferred: 2']);
});expect(root).toMatchRenderedOutput(
<div><div>Original: 2</div><div>Deferred: 2</div></div>,);// But if it updates during a transition, it doesn't deferawait act(async () => {startTransition(() => {root.render(<App value={3} />);
});// The deferred value updates in the same render as the originalawait waitForPaint(['Original: 3', 'Deferred: 3']);
});expect(root).toMatchRenderedOutput(
<div><div>Original: 3</div><div>Deferred: 3</div></div>,);});it('regression test: during urgent update, reuse previous value, not initial value', async () => {function App({value: propValue}) {const [value, setValue] = useState(null);
if (value !== propValue) {setValue(propValue);}const deferredValue = useDeferredValue(value);const child = useMemo(() => <Text text={'Original: ' + value} />,
[value],
);const deferredChild = useMemo(() => <Text text={'Deferred: ' + deferredValue} />,
[deferredValue],
);return (<div><div>{child}</div><div>{deferredChild}</div></div>);}const root = ReactNoop.createRoot();
// Initial renderawait act(async () => {root.render(<App value={1} />);
await waitForPaint(['Original: 1', 'Deferred: 1']);
expect(root).toMatchRenderedOutput(
<div><div>Original: 1</div><div>Deferred: 1</div></div>,);});await act(async () => {startTransition(() => {root.render(<App value={2} />);
});// In the regression, the memoized value was not updated during non-urgent// updates, so this would flip the deferred value back to the initial// value (1) instead of reusing the current one (2).
await waitForPaint(['Original: 2', 'Deferred: 2']);
expect(root).toMatchRenderedOutput(
<div><div>Original: 2</div><div>Deferred: 2</div></div>,);});await act(async () => {root.render(<App value={3} />);
await waitForPaint(['Original: 3']);
expect(root).toMatchRenderedOutput(
<div><div>Original: 3</div><div>Deferred: 2</div></div>,);await waitForPaint(['Deferred: 3']);
expect(root).toMatchRenderedOutput(
<div><div>Original: 3</div><div>Deferred: 3</div></div>,);});});// @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');
});// @gate enableUseDeferredValueInitialArg
it(
'if a suspended render spawns a deferred task, we can switch to the ' +
'deferred task without finishing the original one',
async () => {
function App() {
const text = useDeferredValue('Final', 'Loading...');
return <AsyncText text={text} />;
}const root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog([
'Suspend! [Loading...]',
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
]);expect(root).toMatchRenderedOutput(null);
// The final value loads, so we can skip the initial value entirely.
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
// When the initial value finally loads, nothing happens because we no
// longer need it.
await act(() => resolveText('Loading...'));
assertLog([]);
expect(root).toMatchRenderedOutput('Final');
},);// @gate enableUseDeferredValueInitialArg
it(
'if a suspended render spawns a deferred task that also suspends, we can ' +
'finish the original task if that one loads first',
async () => {
function App() {
const text = useDeferredValue('Final', 'Loading...');
return <AsyncText text={text} />;
}const root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog([
'Suspend! [Loading...]',
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
]);expect(root).toMatchRenderedOutput(null);
// The initial value resolves first, so we render that.
await act(() => resolveText('Loading...'));
assertLog([
'Loading...',
// Still waiting for the final value.
'Suspend! [Final]',
]);expect(root).toMatchRenderedOutput('Loading...');
// The final value loads, so we can switch to that.
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
},);// @gate enableUseDeferredValueInitialArg
it(
'if there are multiple useDeferredValues in the same tree, only the ' +
'first level defers; subsequent ones go straight to the final value, to ' +
'avoid a waterfall',
async () => {
function App() {
const showContent = useDeferredValue(true, false);
if (!showContent) {
return <Text text="App Preview" />;
}return <Content />;
}function Content() {
const text = useDeferredValue('Content', 'Content Preview');
return <AsyncText text={text} />;
}const root = ReactNoop.createRoot();
resolveText('App Preview');
await act(() => root.render(<App />));
assertLog([
// The App shows an immediate preview
'App Preview',
// Then we switch to showing the content. The Content component also
// contains a useDeferredValue, but since we already showed a preview
// in a parent component, we skip the preview in the inner one and
// go straight to attempting the final value.
//
// (Note that this is intentionally different from how nested Suspense
// boundaries work, where we always prefer to show the innermost
// loading state.)
'Suspend! [Content]',
]);// Still showing the App preview state because the inner
// content suspended.
expect(root).toMatchRenderedOutput('App Preview');
// Finish loading the content
await act(() => resolveText('Content'));
// We didn't even attempt to render Content Preview.
assertLog(['Content']);
expect(root).toMatchRenderedOutput('Content');
},);// @gate enableUseDeferredValueInitialArg
it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
// Same as the previous test but with a Suspense boundary separating the
// two useDeferredValue hooks.
function App() {
const showContent = useDeferredValue(true, false);
if (!showContent) {
return <Text text="App Preview" />;
}return (
<Suspense fallback={<Text text="Loading..." />}>
<Content />
</Suspense>
);}function Content() {const text = useDeferredValue('Content', 'Content Preview');return <AsyncText text={text} />;
}const root = ReactNoop.createRoot();
resolveText('App Preview');
await act(() => root.render(<App />));
assertLog([
// The App shows an immediate preview
'App Preview',
// Then we switch to showing the content. The Content component also
// contains a useDeferredValue, but since we already showed a preview
// in a parent component, we skip the preview in the inner one and
// go straight to attempting the final value.
'Suspend! [Content]',
'Loading...',
]);// The content suspended, so we show a Suspense fallback
expect(root).toMatchRenderedOutput('Loading...');
// Finish loading the content
await act(() => resolveText('Content'));
// We didn't even attempt to render Content Preview.
assertLog(['Content']);
expect(root).toMatchRenderedOutput('Content');
});// @gate enableUseDeferredValueInitialArg
// @gate enableActivity
it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => {
function App() {
const text = useDeferredValue('Final', 'Preview');
return (
<div>
<AsyncText text={text} />
</div>
);}let revealContent;function Container({children}) {const [shouldShow, setState] = useState(false);
revealContent = () => setState(true);return (<Activity mode={shouldShow ? 'visible' : 'hidden'}>{children}</Activity>
);}const root = ReactNoop.createRoot();
// Prerender a hidden tree
resolveText('Preview');
await act(() =>
root.render(
<Container>
<App />
</Container>,
),);assertLog(['Preview', 'Suspend! [Final]']);
expect(root).toMatchRenderedOutput(<div hidden={true}>Preview</div>);
// Finish loading the content
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput(<div hidden={true}>Final</div>);
// Now reveal the hidden tree. It should toggle the visibility without
// having to re-render anything inside the prerendered tree.
await act(() => revealContent());
assertLog([]);
expect(root).toMatchRenderedOutput(<div>Final</div>);
});// @gate enableUseDeferredValueInitialArg
// @gate enableActivity
it('useDeferredValue can prerender the initial value inside a hidden tree', async () => {
function App({text}) {
const renderedText = useDeferredValue(text, `Preview [${text}]`);
return (
<div>
<Text text={renderedText} />
</div>
);}let revealContent;function Container({children}) {const [shouldShow, setState] = useState(false);
revealContent = () => setState(true);return (<Activity mode={shouldShow ? 'visible' : 'hidden'}>{children}</Activity>
);}const root = ReactNoop.createRoot();
// Prerender some content
await act(() => {
root.render(
<Container>
<App text="A" />
</Container>,
);});assertLog(['Preview [A]', 'A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
await act(async () => {// While the tree is still hidden, update the pre-rendered tree.
root.render(
<Container><App text="B" /></Container>,);// We should switch to pre-rendering the new preview.
await waitForPaint(['Preview [B]']);
expect(root).toMatchRenderedOutput(<div hidden={true}>Preview [B]</div>);
// Before the prerender is complete, reveal the hidden tree. Because we
// consider revealing a hidden tree to be the same as mounting a new one,// we should not skip the preview state.
revealContent();// Because the preview state was already prerendered, we can reveal it// without any addditional work.
await waitForPaint([]);
expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
});// Finally, finish rendering the final value.
assertLog(['B']);
expect(root).toMatchRenderedOutput(<div>B</div>);
});// @gate enableUseDeferredValueInitialArg
// @gate enableActivity
it(
'useDeferredValue skips the preview state when revealing a hidden tree ' +
'if the final value is referentially identical',
async () => {
function App({text}) {
const renderedText = useDeferredValue(text, `Preview [${text}]`);
return (
<div>
<Text text={renderedText} />
</div>
);}function Container({text, shouldShow}) {return (<Activity mode={shouldShow ? 'visible' : 'hidden'}>
<App text={text} /></Activity>);}const root = ReactNoop.createRoot();
// Prerender some content
await act(() => root.render(<Container text="A" shouldShow={false} />));
assertLog(['Preview [A]', 'A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
// Reveal the prerendered tree. Because the final value is referentially
// equal to what was already prerendered, we can skip the preview state
// and go straight to the final one. The practical upshot of this is
// that we can completely prerender the final value without having to
// do additional rendering work when the tree is revealed.
await act(() => root.render(<Container text="A" shouldShow={true} />));
assertLog(['A']);
expect(root).toMatchRenderedOutput(<div>A</div>);
},);// @gate enableUseDeferredValueInitialArg
// @gate enableActivity
it(
'useDeferredValue does not skip the preview state when revealing a ' +
'hidden tree if the final value is different from the currently rendered one',
async () => {
function App({text}) {
const renderedText = useDeferredValue(text, `Preview [${text}]`);
return (
<div>
<Text text={renderedText} />
</div>
);}function Container({text, shouldShow}) {return (<Activity mode={shouldShow ? 'visible' : 'hidden'}>
<App text={text} /></Activity>);}const root = ReactNoop.createRoot();
// Prerender some content
await act(() => root.render(<Container text="A" shouldShow={false} />));
assertLog(['Preview [A]', 'A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
// Reveal the prerendered tree. Because the final value is different from
// what was already prerendered, we can't bail out. Since we treat
// revealing a hidden tree the same as a new mount, show the preview state
// before switching to the final one.
await act(async () => {
root.render(<Container text="B" shouldShow={true} />);
// First commit the preview state
await waitForPaint(['Preview [B]']);
expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
});// Then switch to the final state
assertLog(['B']);
expect(root).toMatchRenderedOutput(<div>B</div>);
},);// @gate enableActivity
it(
'useDeferredValue does not show "previous" value when revealing a hidden ' +
'tree (no initial value)',
async () => {
function App({text}) {
const renderedText = useDeferredValue(text);
return (
<div>
<Text text={renderedText} />
</div>
);}function Container({text, shouldShow}) {return (<Activity mode={shouldShow ? 'visible' : 'hidden'}>
<App text={text} /></Activity>);}const root = ReactNoop.createRoot();
// Prerender some content
await act(() => root.render(<Container text="A" shouldShow={false} />));
assertLog(['A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
// Update the prerendered tree and reveal it at the same time. Even though
// this is a sync update, we should update B immediately rather than stay
// on the old value (A), because conceptually this is a new tree.
await act(() => root.render(<Container text="B" shouldShow={true} />));
assertLog(['B']);
expect(root).toMatchRenderedOutput(<div>B</div>);
},);});