'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}) {
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();
await act(() => {
root.render(<App value={1} />);
});
assertLog(['Original: 1', 'Deferred: 1']);
await act(async () => {
root.render(<App value={2} />);
await waitForPaint(['Original: 2']);
await waitForPaint(['Deferred: 2']);
});
expect(root).toMatchRenderedOutput(
<div>
<div>Original: 2</div>
<div>Deferred: 2</div>
</div>,
);
await act(async () => {
startTransition(() => {
root.render(<App value={3} />);
});
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();
await act(() => {
root.render(<App value={1} />);
});
assertLog(['Original: 1', 'Deferred: 1']);
await act(async () => {
root.render(<App value={2} />);
await waitForPaint(['Original: 2']);
await waitForPaint(['Deferred: 2']);
});
expect(root).toMatchRenderedOutput(
<div>
<div>Original: 2</div>
<div>Deferred: 2</div>
</div>,
);
await act(async () => {
startTransition(() => {
root.render(<App value={3} />);
});
await 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();
await act(() => {
root.render(<App value={1} />);
});
assertLog(['Original: 1', 'Deferred: 1']);
await act(async () => {
root.render(<App value={2} />);
await waitForPaint(['Original: 2']);
await waitForPaint(['Deferred: 2']);
});
expect(root).toMatchRenderedOutput(
<div>
<div>Original: 2</div>
<div>Deferred: 2</div>
</div>,
);
await act(async () => {
startTransition(() => {
root.render(<App value={3} />);
});
await 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();
await 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} />);
});
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>,
);
});
});
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');
});
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 () => {
startTransition(() => root.render(<App />));
await waitForPaint(['Initial']);
expect(root).toMatchRenderedOutput('Initial');
});
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
});
it(
'if a suspended render spawns a deferred task, we can switch to the ' +
'deferred task without finishing the original one (no Suspense boundary)',
async () => {
function App() {
const text = useDeferredValue('Final', 'Loading...');
return <AsyncText text={text} />;
}
const root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog([
'Suspend! [Loading...]',
'Suspend! [Final]',
]);
expect(root).toMatchRenderedOutput(null);
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
await act(() => resolveText('Loading...'));
assertLog([]);
expect(root).toMatchRenderedOutput('Final');
},
);
it(
'if a suspended render spawns a deferred task, we can switch to the ' +
'deferred task without finishing the original one (no Suspense boundary, ' +
'synchronous parent update)',
async () => {
function App() {
const text = useDeferredValue('Final', 'Loading...');
return <AsyncText text={text} />;
}
const root = ReactNoop.createRoot();
await act(() => {
ReactNoop.flushSync(() => root.render(<App />));
});
assertLog([
'Suspend! [Loading...]',
'Suspend! [Final]',
]);
expect(root).toMatchRenderedOutput(null);
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
await act(() => resolveText('Loading...'));
assertLog([]);
expect(root).toMatchRenderedOutput('Final');
},
);
it(
'if a suspended render spawns a deferred task, we can switch to the ' +
'deferred task without finishing the original one (Suspense boundary)',
async () => {
function App() {
const text = useDeferredValue('Final', 'Loading...');
return <AsyncText text={text} />;
}
const root = ReactNoop.createRoot();
await act(() =>
root.render(
<Suspense fallback={<Text text="Fallback" />}>
<App />
</Suspense>,
),
);
assertLog([
'Suspend! [Loading...]',
'Fallback',
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput('Fallback');
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
await act(() => resolveText('Loading...'));
assertLog([]);
expect(root).toMatchRenderedOutput('Final');
},
);
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...]',
'Suspend! [Final]',
]);
expect(root).toMatchRenderedOutput(null);
await act(() => resolveText('Loading...'));
assertLog([
'Loading...',
'Suspend! [Final]',
]);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput('Final');
},
);
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([
'App Preview',
'Suspend! [Content]',
]);
expect(root).toMatchRenderedOutput('App Preview');
await act(() => resolveText('Content'));
assertLog(['Content']);
expect(root).toMatchRenderedOutput('Content');
},
);
it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
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([
'App Preview',
'Suspend! [Content]',
'Loading...',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Content]'] : []),
]);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => resolveText('Content'));
assertLog(['Content']);
expect(root).toMatchRenderedOutput('Content');
});
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();
resolveText('Preview');
await act(() =>
root.render(
<Container>
<App />
</Container>,
),
);
assertLog(['Preview', 'Suspend! [Final]']);
expect(root).toMatchRenderedOutput(<div hidden={true}>Preview</div>);
await act(() => resolveText('Final'));
assertLog(['Final']);
expect(root).toMatchRenderedOutput(<div hidden={true}>Final</div>);
await act(() => revealContent());
assertLog([]);
expect(root).toMatchRenderedOutput(<div>Final</div>);
});
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();
await act(() => {
root.render(
<Container>
<App text="A" />
</Container>,
);
});
assertLog(['Preview [A]', 'A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
await act(async () => {
root.render(
<Container>
<App text="B" />
</Container>,
);
await waitForPaint(['Preview [B]']);
expect(root).toMatchRenderedOutput(<div hidden={true}>Preview [B]</div>);
revealContent();
await waitForPaint([]);
expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
});
assertLog(['B']);
expect(root).toMatchRenderedOutput(<div>B</div>);
});
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();
await act(() => root.render(<Container text="A" shouldShow={false} />));
assertLog(['Preview [A]', 'A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
await act(() => root.render(<Container text="A" shouldShow={true} />));
assertLog(['A']);
expect(root).toMatchRenderedOutput(<div>A</div>);
},
);
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();
await act(() => root.render(<Container text="A" shouldShow={false} />));
assertLog(['Preview [A]', 'A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
await act(async () => {
root.render(<Container text="B" shouldShow={true} />);
await waitForPaint(['Preview [B]']);
expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
});
assertLog(['B']);
expect(root).toMatchRenderedOutput(<div>B</div>);
},
);
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();
await act(() => root.render(<Container text="A" shouldShow={false} />));
assertLog(['A']);
expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
await act(() => root.render(<Container text="B" shouldShow={true} />));
assertLog(['B']);
expect(root).toMatchRenderedOutput(<div>B</div>);
},
);
});