'use strict';
let React;
let ReactNoop;
let Scheduler;
let act;
let use;
let useDebugValue;
let useState;
let useMemo;
let useEffect;
let Suspense;
let startTransition;
let cache;
let pendingTextRequests;
let waitFor;
let waitForPaint;
let assertLog;
let waitForAll;
describe('ReactUse', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
use = React.use;
useDebugValue = React.useDebugValue;
useState = React.useState;
useMemo = React.useMemo;
useEffect = React.useEffect;
Suspense = React.Suspense;
startTransition = React.startTransition;
cache = React.cache;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
waitFor = InternalTestUtils.waitFor;
pendingTextRequests = new Map();
});
function resolveTextRequests(text) {
const requests = pendingTextRequests.get(text);
if (requests !== undefined) {
pendingTextRequests.delete(text);
requests.forEach(resolve => resolve(text));
}
}
function getAsyncText(text) {
Scheduler.log(`Async text requested [${text}]`);
return new Promise(resolve => {
const requests = pendingTextRequests.get(text);
if (requests !== undefined) {
requests.push(resolve);
pendingTextRequests.set(text, requests);
} else {
pendingTextRequests.set(text, [resolve]);
}
});
}
function Text({text}) {
Scheduler.log(text);
return text;
}
test('if suspended fiber is pinged in a microtask, retry immediately without unwinding the stack', async () => {
let fulfilled = false;
function Async() {
if (fulfilled) {
return <Text text="Async" />;
}
Scheduler.log('Suspend!');
throw Promise.resolve().then(() => {
Scheduler.log('Resolve in microtask');
fulfilled = true;
});
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog([
'Suspend!',
'Resolve in microtask',
'Async',
]);
expect(root).toMatchRenderedOutput('Async');
});
test('if suspended fiber is pinged in a microtask, it does not block a transition from completing', async () => {
let fulfilled = false;
function Async() {
if (fulfilled) {
return <Text text="Async" />;
}
Scheduler.log('Suspend!');
throw Promise.resolve().then(() => {
Scheduler.log('Resolve in microtask');
fulfilled = true;
});
}
function App() {
return <Async />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['Suspend!', 'Resolve in microtask', 'Async']);
expect(root).toMatchRenderedOutput('Async');
});
test('does not infinite loop if already fulfilled thenable is thrown', async () => {
const thenable = {
then(ping) {},
status: 'fulfilled',
value: null,
};
let i = 0;
function Async() {
if (i++ > 50) {
throw new Error('Infinite loop detected');
}
Scheduler.log('Suspend!');
throw thenable;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['Suspend!', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
});
test('basic use(promise)', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.resolve('B');
const promiseC = Promise.resolve('C');
function Async() {
const text = use(promiseA) + use(promiseB) + use(promiseC);
return <Text text={text} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['ABC']);
expect(root).toMatchRenderedOutput('ABC');
});
test("using a promise that's not cached between attempts", async () => {
function Async() {
const text =
use(Promise.resolve('A')) +
use(Promise.resolve('B')) +
use(Promise.resolve('C'));
return <Text text={text} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['ABC']);
expect(root).toMatchRenderedOutput('ABC');
});
test('using a rejected promise will throw', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <Text text={this.state.error.message} />;
}
return this.props.children;
}
}
const promiseA = Promise.resolve('A');
const promiseB = Promise.reject(new Error('Oops!'));
const promiseC = Promise.resolve('C');
await expect(promiseB).rejects.toThrow('Oops!');
function Async() {
const text = use(promiseA) + use(promiseB) + use(promiseC);
return <Text text={text} />;
}
function App() {
return (
<ErrorBoundary>
<Async />
</ErrorBoundary>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['Oops!', 'Oops!']);
});
test('use(promise) in multiple components', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.resolve('B');
const promiseC = Promise.resolve('C');
const promiseD = Promise.resolve('D');
function Child({prefix}) {
return <Text text={prefix + use(promiseC) + use(promiseD)} />;
}
function Parent() {
return <Child prefix={use(promiseA) + use(promiseB)} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Parent />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['ABCD']);
expect(root).toMatchRenderedOutput('ABCD');
});
test('use(promise) in multiple sibling components', async () => {
const promiseA = {then: () => {}, status: 'pending', value: null};
const promiseB = {then: () => {}, status: 'pending', value: null};
const promiseC = {then: () => {}, status: 'fulfilled', value: 'C'};
const promiseD = {then: () => {}, status: 'fulfilled', value: 'D'};
function Sibling1({prefix}) {
return <Text text={use(promiseA) + use(promiseB)} />;
}
function Sibling2() {
return <Text text={use(promiseC) + use(promiseD)} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Sibling1 />
<Sibling2 />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['CD', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
});
test('erroring in the same component as an uncached promise does not result in an infinite loop', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <Text text={'Caught an error: ' + this.state.error.message} />;
}
return this.props.children;
}
}
let i = 0;
function Async({
// Intentionally destrucutring a prop here so that our production error
// stack trick is triggered at the beginning of the function
prop,
}) {
if (i++ > 50) {
throw new Error('Infinite loop detected');
}
try {
use(Promise.resolve('Async'));
} catch (e) {
Scheduler.log('Suspend! [Async]');
throw e;
}
throw new Error('Oops!');
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<ErrorBoundary>
<Async />
</ErrorBoundary>
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog([
'Suspend! [Async]',
'Caught an error: Oops!',
'Suspend! [Async]',
'Loading...',
'Suspend! [Async]',
'Caught an error: Oops!',
]);
expect(root).toMatchRenderedOutput('Caught an error: Oops!');
});
test('basic use(context)', async () => {
const ContextA = React.createContext('');
const ContextB = React.createContext('B');
function Sync() {
const text = use(ContextA) + use(ContextB);
return text;
}
function App() {
return (
<ContextA.Provider value="A">
<Sync />
</ContextA.Provider>
);
}
const root = ReactNoop.createRoot();
root.render(<App />);
await waitForAll([]);
expect(root).toMatchRenderedOutput('AB');
});
test('interrupting while yielded should reset contexts', async () => {
let resolve;
const promise = new Promise(r => {
resolve = r;
});
const Context = React.createContext();
const lazy = React.lazy(() => {
return promise;
});
function ContextText() {
return <Text text={use(Context)} />;
}
function App({text}) {
return (
<div>
<Context.Provider value={text}>
{lazy}
<ContextText />
</Context.Provider>
</div>
);
}
const root = ReactNoop.createRoot();
startTransition(() => {
root.render(<App text="world" />);
});
await waitForPaint([]);
expect(root).toMatchRenderedOutput(null);
await resolve({default: <Text key="hi" text="Hello " />});
ReactNoop.flushSync(() => {
root.render(<App text="world!" />);
});
assertLog(['Hello ', 'world!']);
expect(root).toMatchRenderedOutput(<div>Hello world!</div>);
});
test('warns if use(promise) is wrapped with try/catch block', async () => {
function Async() {
try {
return <Text text={use(Promise.resolve('Async'))} />;
} catch (e) {
return <Text text="Fallback" />;
}
}
spyOnDev(console, 'error').mockImplementation(() => {});
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error.mock.calls[0][0]).toContain(
'Warning: `use` was called from inside a try/catch block. This is not ' +
'allowed and can lead to unexpected behavior. To handle errors ' +
'triggered by `use`, wrap your component in a error boundary.',
);
}
});
test('during a transition, can unwrap async operations even if nothing is cached', async () => {
function App() {
return <Text text={use(getAsyncText('Async'))} />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="(empty)" />
</Suspense>,
);
});
assertLog(['(empty)']);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
});
assertLog(['Async text requested [Async]']);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
resolveTextRequests('Async');
});
assertLog(['Async text requested [Async]', 'Async']);
expect(root).toMatchRenderedOutput('Async');
});
test("does not prevent a Suspense fallback from showing if it's a new boundary, even during a transition", async () => {
function App() {
return <Text text={use(getAsyncText('Async'))} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
});
assertLog(['Async text requested [Async]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveTextRequests('Async');
});
assertLog(['Async text requested [Async]']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveTextRequests('Async');
});
assertLog(['Async text requested [Async]', 'Async']);
expect(root).toMatchRenderedOutput('Async');
});
test('when waiting for data to resolve, a fresh update will trigger a restart', async () => {
function App() {
return <Text text={use(getAsyncText('Will never resolve'))} />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Suspense fallback={<Text text="Loading..." />} />);
});
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
});
assertLog(['Async text requested [Will never resolve]']);
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Text text="Something different" />
</Suspense>,
);
});
assertLog(['Something different']);
});
test('when waiting for data to resolve, an update on a different root does not cause work to be dropped', async () => {
const getCachedAsyncText = cache(getAsyncText);
function App() {
return <Text text={use(getCachedAsyncText('Hi'))} />;
}
const root1 = ReactNoop.createRoot();
await act(() => {
root1.render(<Suspense fallback={<Text text="Loading..." />} />);
});
await act(() => {
startTransition(() => {
root1.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
});
assertLog(['Async text requested [Hi]']);
const root2 = ReactNoop.createRoot();
await act(() => {
root2.render('Do re mi');
});
expect(root2).toMatchRenderedOutput('Do re mi');
await act(async () => {
await resolveTextRequests('Hi');
});
assertLog(['Hi']);
expect(root1).toMatchRenderedOutput('Hi');
});
test('while suspended, hooks cannot be called (i.e. current dispatcher is unset correctly)', async () => {
function App() {
return <Text text={use(getAsyncText('Will never resolve'))} />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Suspense fallback={<Text text="Loading..." />} />);
});
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
});
assertLog(['Async text requested [Will never resolve]']);
expect(useState).toThrow(
'Invalid hook call. Hooks can only be called inside of the body of a ' +
'function component.',
);
});
test('unwraps thenable that fulfills synchronously without suspending', async () => {
function App() {
const thenable = {
then(resolve) {
resolve('Hi');
},
};
try {
return <Text text={use(thenable)} />;
} catch {
throw new Error(
'`use` should not suspend because the thenable resolved synchronously.',
);
}
}
const root = ReactNoop.createRoot();
ReactNoop.flushSync(() => {
root.render(<App />);
});
assertLog(['Hi']);
expect(root).toMatchRenderedOutput('Hi');
});
test('does not suspend indefinitely if an interleaved update was skipped', async () => {
function Child({childShouldSuspend}) {
return (
<Text
text={
childShouldSuspend
? use(getAsyncText('Will never resolve'))
: 'Child'
}
/>
);
}
let setChildShouldSuspend;
let setShowChild;
function Parent() {
const [showChild, _setShowChild] = useState(true);
setShowChild = _setShowChild;
const [childShouldSuspend, _setChildShouldSuspend] = useState(false);
setChildShouldSuspend = _setChildShouldSuspend;
Scheduler.log(
`childShouldSuspend: ${childShouldSuspend}, showChild: ${showChild}`,
);
return showChild ? (
<Child childShouldSuspend={childShouldSuspend} />
) : (
<Text text="(empty)" />
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Parent />);
});
assertLog(['childShouldSuspend: false, showChild: true', 'Child']);
expect(root).toMatchRenderedOutput('Child');
await act(async () => {
startTransition(() => {
setChildShouldSuspend(true);
});
await waitFor(['childShouldSuspend: true, showChild: true']);
startTransition(() => {
setShowChild(false);
});
});
assertLog([
'Async text requested [Will never resolve]',
'childShouldSuspend: false, showChild: false',
'(empty)',
'childShouldSuspend: true, showChild: false',
'(empty)',
]);
expect(root).toMatchRenderedOutput('(empty)');
});
test('when replaying a suspended component, reuses the hooks computed during the previous attempt (Memo)', async () => {
function ExcitingText({text}) {
const uppercaseText = useMemo(() => {
Scheduler.log('Compute uppercase: ' + text);
return text.toUpperCase();
}, [text]);
const exclamatoryText = use(getAsyncText(uppercaseText + '!'));
const sparklingText = useMemo(() => {
Scheduler.log('Add sparkles: ' + exclamatoryText);
return `✨ ${exclamatoryText} ✨`;
}, [exclamatoryText]);
return <Text text={sparklingText} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<ExcitingText text="Hello" />);
});
});
assertLog(['Compute uppercase: Hello', 'Async text requested [HELLO!]']);
expect(root).toMatchRenderedOutput(null);
await act(() => {
resolveTextRequests('HELLO!');
});
assertLog([
'Async text requested [HELLO!]',
'Add sparkles: HELLO!',
'✨ HELLO! ✨',
]);
});
test('when replaying a suspended component, reuses the hooks computed during the previous attempt (State)', async () => {
let _setFruit;
let _setVegetable;
function Kitchen() {
const [fruit, setFruit] = useState('apple');
_setFruit = setFruit;
const usedFruit = use(getAsyncText(fruit));
const [vegetable, setVegetable] = useState('carrot');
_setVegetable = setVegetable;
return <Text text={usedFruit + ' ' + vegetable} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<Kitchen />);
});
});
assertLog(['Async text requested [apple]']);
expect(root).toMatchRenderedOutput(null);
await act(() => {
resolveTextRequests('apple');
});
assertLog(['Async text requested [apple]', 'apple carrot']);
expect(root).toMatchRenderedOutput('apple carrot');
await act(() => {
startTransition(() => {
_setVegetable('dill');
});
});
assertLog(['Async text requested [apple]']);
expect(root).toMatchRenderedOutput('apple carrot');
await act(() => {
resolveTextRequests('apple');
});
assertLog(['Async text requested [apple]', 'apple dill']);
expect(root).toMatchRenderedOutput('apple dill');
await act(() => {
startTransition(() => {
_setFruit('banana');
});
});
assertLog(['Async text requested [banana]']);
expect(root).toMatchRenderedOutput('apple dill');
await act(() => {
resolveTextRequests('banana');
});
assertLog(['Async text requested [banana]', 'banana dill']);
expect(root).toMatchRenderedOutput('banana dill');
});
test('when replaying a suspended component, reuses the hooks computed during the previous attempt (DebugValue+State)', async () => {
let _setLawyer;
function Lexicon() {
useDebugValue(123);
const avocado = use(getAsyncText('aguacate'));
const [lawyer, setLawyer] = useState('abogado');
_setLawyer = setLawyer;
return <Text text={avocado + ' ' + lawyer} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<Lexicon />);
});
});
assertLog(['Async text requested [aguacate]']);
expect(root).toMatchRenderedOutput(null);
await act(() => {
resolveTextRequests('aguacate');
});
assertLog(['Async text requested [aguacate]', 'aguacate abogado']);
expect(root).toMatchRenderedOutput('aguacate abogado');
await act(() => {
startTransition(() => {
_setLawyer('avocat');
});
});
assertLog(['Async text requested [aguacate]']);
expect(root).toMatchRenderedOutput('aguacate abogado');
await act(() => {
resolveTextRequests('aguacate');
});
assertLog(['Async text requested [aguacate]', 'aguacate avocat']);
expect(root).toMatchRenderedOutput('aguacate avocat');
});
test(
'wrap an async function with useMemo to skip running the function ' +
'twice when loading new data',
async () => {
function App({text}) {
const promiseForText = useMemo(async () => getAsyncText(text), [text]);
const asyncText = use(promiseForText);
return <Text text={asyncText} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App text="Hello" />);
});
});
assertLog(['Async text requested [Hello]']);
expect(root).toMatchRenderedOutput(null);
await act(() => {
resolveTextRequests('Hello');
});
assertLog([
'Hello',
]);
},
);
test('load multiple nested Suspense boundaries', async () => {
const getCachedAsyncText = cache(getAsyncText);
function AsyncText({text}) {
return <Text text={use(getCachedAsyncText(text))} />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="(Loading A...)" />}>
<AsyncText text="A" />
<Suspense fallback={<Text text="(Loading B...)" />}>
<AsyncText text="B" />
<Suspense fallback={<Text text="(Loading C...)" />}>
<AsyncText text="C" />
</Suspense>
</Suspense>
</Suspense>,
);
});
assertLog([
'Async text requested [A]',
'Async text requested [B]',
'Async text requested [C]',
'(Loading C...)',
'(Loading B...)',
'(Loading A...)',
]);
expect(root).toMatchRenderedOutput('(Loading A...)');
await act(() => {
resolveTextRequests('A');
});
assertLog(['A', '(Loading C...)', '(Loading B...)']);
expect(root).toMatchRenderedOutput('A(Loading B...)');
await act(() => {
resolveTextRequests('B');
});
assertLog(['B', '(Loading C...)']);
expect(root).toMatchRenderedOutput('AB(Loading C...)');
await act(() => {
resolveTextRequests('C');
});
assertLog(['C']);
expect(root).toMatchRenderedOutput('ABC');
});
test('load multiple nested Suspense boundaries (uncached requests)', async () => {
function AsyncText({text}) {
return <Text text={use(getAsyncText(text))} />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="(Loading A...)" />}>
<AsyncText text="A" />
<Suspense fallback={<Text text="(Loading B...)" />}>
<AsyncText text="B" />
<Suspense fallback={<Text text="(Loading C...)" />}>
<AsyncText text="C" />
</Suspense>
</Suspense>
</Suspense>,
);
});
assertLog([
'Async text requested [A]',
'Async text requested [B]',
'Async text requested [C]',
'(Loading C...)',
'(Loading B...)',
'(Loading A...)',
]);
expect(root).toMatchRenderedOutput('(Loading A...)');
await act(() => {
resolveTextRequests('A');
});
assertLog(['Async text requested [A]']);
expect(root).toMatchRenderedOutput('(Loading A...)');
await act(() => {
resolveTextRequests('A');
});
assertLog([
'Async text requested [A]',
'A',
'Async text requested [B]',
'Async text requested [C]',
'(Loading C...)',
'(Loading B...)',
]);
expect(root).toMatchRenderedOutput('A(Loading B...)');
await act(() => {
resolveTextRequests('B');
});
assertLog(['Async text requested [B]']);
expect(root).toMatchRenderedOutput('A(Loading B...)');
await act(() => {
resolveTextRequests('B');
});
assertLog([
'Async text requested [B]',
'B',
'Async text requested [C]',
'(Loading C...)',
]);
expect(root).toMatchRenderedOutput('AB(Loading C...)');
await act(() => {
resolveTextRequests('C');
});
assertLog(['Async text requested [C]']);
expect(root).toMatchRenderedOutput('AB(Loading C...)');
await act(() => {
resolveTextRequests('C');
});
assertLog(['Async text requested [C]', 'C']);
expect(root).toMatchRenderedOutput('ABC');
});
test('use() combined with render phase updates', async () => {
function Async() {
const a = use(Promise.resolve('A'));
const [count, setCount] = useState(0);
if (count === 0) {
setCount(1);
}
const usedCount = use(Promise.resolve(count));
return <Text text={a + usedCount} />;
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['A1']);
expect(root).toMatchRenderedOutput('A1');
});
test('basic promise as child', async () => {
const promise = Promise.resolve(<Text text="Hi" />);
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(promise);
});
});
assertLog(['Hi']);
expect(root).toMatchRenderedOutput('Hi');
});
test('basic async component', async () => {
async function App() {
await getAsyncText('Hi');
return <Text text="Hi" />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['Async text requested [Hi]']);
await act(() => resolveTextRequests('Hi'));
assertLog([
'Async text requested [Hi]',
'Hi',
]);
expect(root).toMatchRenderedOutput('Hi');
});
test('async child of a non-function component (e.g. a class)', async () => {
class App extends React.Component {
async render() {
const text = await getAsyncText('Hi');
return <Text text={text} />;
}
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<App />);
});
});
assertLog(['Async text requested [Hi]']);
await act(async () => resolveTextRequests('Hi'));
assertLog([
'Async text requested [Hi]',
'Hi',
]);
expect(root).toMatchRenderedOutput('Hi');
});
test('async children are recursively unwrapped', async () => {
const thenable = {
then() {},
status: 'fulfilled',
value: {
then() {},
status: 'fulfilled',
value: <Text text="Hi" />,
},
};
const root = ReactNoop.createRoot();
await act(() => {
root.render(thenable);
});
assertLog(['Hi']);
expect(root).toMatchRenderedOutput('Hi');
});
test('async children are transparently unwrapped before being reconciled (top level)', async () => {
function Child({text}) {
useEffect(() => {
Scheduler.log(`Mount: ${text}`);
}, [text]);
return <Text text={text} />;
}
async function App({text}) {
return <Child text={text} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App text="A" />);
});
});
assertLog(['A', 'Mount: A']);
expect(root).toMatchRenderedOutput('A');
await act(() => {
startTransition(() => {
root.render(<App text="B" />);
});
});
assertLog(['B', 'Mount: B']);
expect(root).toMatchRenderedOutput('B');
});
test('async children are transparently unwrapped before being reconciled (siblings)', async () => {
function Child({text}) {
useEffect(() => {
Scheduler.log(`Mount: ${text}`);
}, [text]);
return <Text text={text} />;
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(
<>
{Promise.resolve(<Child text="A" />)}
{Promise.resolve(<Child text="B" />)}
{Promise.resolve(<Child text="C" />)}
</>,
);
});
});
assertLog(['A', 'B', 'C', 'Mount: A', 'Mount: B', 'Mount: C']);
expect(root).toMatchRenderedOutput('ABC');
await act(() => {
startTransition(() => {
root.render(
<>
{Promise.resolve(<Child text="A" />)}
{Promise.resolve(<Child text="B" />)}
{Promise.resolve(<Child text="C" />)}
</>,
);
});
});
assertLog(['A', 'B', 'C']);
expect(root).toMatchRenderedOutput('ABC');
});
test('async children are transparently unwrapped before being reconciled (siblings, reordered)', async () => {
function Child({text}) {
useEffect(() => {
Scheduler.log(`Mount: ${text}`);
}, [text]);
return <Text text={text} />;
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(
<>
{Promise.resolve(<Child key="A" text="A" />)}
{Promise.resolve(<Child key="B" text="B" />)}
{Promise.resolve(<Child key="C" text="C" />)}
</>,
);
});
});
assertLog(['A', 'B', 'C', 'Mount: A', 'Mount: B', 'Mount: C']);
expect(root).toMatchRenderedOutput('ABC');
await act(() => {
startTransition(() => {
root.render(
<>
{Promise.resolve(<Child key="B" text="B" />)}
{Promise.resolve(<Child key="A" text="A" />)}
{Promise.resolve(<Child key="C" text="C" />)}
</>,
);
});
});
assertLog(['B', 'A', 'C']);
expect(root).toMatchRenderedOutput('BAC');
});
test('basic Context as node', async () => {
const Context = React.createContext(null);
function Indirection({children}) {
Scheduler.log('Indirection');
return children;
}
function ParentOfContextNode() {
Scheduler.log('ParentOfContextNode');
return Context;
}
function Child({text}) {
useEffect(() => {
Scheduler.log('Mount');
return () => {
Scheduler.log('Unmount');
};
}, []);
return <Text text={text} />;
}
function App({contextValue, children}) {
const memoizedChildren = useMemo(
() => (
<Indirection>
<ParentOfContextNode />
</Indirection>
),
[children],
);
return (
<Context.Provider value={contextValue}>
{memoizedChildren}
</Context.Provider>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App contextValue={<Child text="A" />} />);
});
assertLog(['Indirection', 'ParentOfContextNode', 'A', 'Mount']);
expect(root).toMatchRenderedOutput('A');
await act(async () => {
root.render(<App contextValue={<Child text="B" />} />);
});
assertLog([
'ParentOfContextNode',
'B',
]);
expect(root).toMatchRenderedOutput('B');
await act(async () => {
root.render(<App contextValue={<Child key="C" text="C" />} />);
});
assertLog([
'ParentOfContextNode',
'C',
'Unmount',
'Mount',
]);
});
test('context as node, at the root', async () => {
const Context = React.createContext(<Text text="Hi" />);
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(Context);
});
});
assertLog(['Hi']);
expect(root).toMatchRenderedOutput('Hi');
});
test('promises that resolves to a context, rendered as a node', async () => {
const Context = React.createContext(<Text text="Hi" />);
const promise = Promise.resolve(Context);
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(promise);
});
});
assertLog(['Hi']);
expect(root).toMatchRenderedOutput('Hi');
});
});