'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;
let waitForMicrotasks;
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;
waitForMicrotasks = InternalTestUtils.waitForMicrotasks;
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) {
// getAsyncText is completely uncached — it performs a new async operation
// every time it's called. During a transition, React should be able to
// unwrap it anyway.
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;
}// This behavior was intentionally disabled to derisk the rollout of `use`.
// It changes the behavior of old, pre-`use` Suspense implementations. We may
// add this back; however, the plan is to migrate all existing Suspense code
// to `use`, so the extra code probably isn't worth it.
// @gate TODO
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([
// React will yield when the async component suspends.'Suspend!','Resolve in microtask',// Finished rendering without unwinding the stack or preparing a fallback.'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 () => {// An already fulfilled promise should never be thrown. Since it already
// fulfilled, we shouldn't bother trying to render again — doing so would// likely lead to an infinite loop. This scenario should only happen if a
// userspace Suspense library makes an implementation mistake.
// Create an already fulfilled thenableconst thenable = {then(ping) {},status: 'fulfilled',value: null,};let i = 0;function Async() {if (i++ > 50) {
throw new Error('Infinite loop detected');}Scheduler.log('Suspend!');
// This thenable should never be thrown because it already fulfilled.
// But if it is thrown, React should handle it gracefully.
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');
// Jest/Node will raise an unhandled rejected error unless we await this. It
// works fine in the browser, though.
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 () => {// This tests that the state for tracking promises is reset per component.
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 () => {// This tests that the state for tracking promises is reset per component.
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(['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 functionprop,}) {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([
// First attempt. The uncached promise suspends.'Suspend! [Async]',
// Because the promise already fulfilled, we're able to unwrap the value// immediately in a microtask.
//// Then we proceed to the rest of the component, which throws an error.
'Caught an error: Oops!',// During the sync error recovery pass, the component suspends, because// we were unable to unwrap the value of the promise.
'Suspend! [Async]',
'Loading...',
// Because the error recovery attempt suspended, React can't tell if the// error was actually fixed, or it was masked by the suspended data.
// In this case, it wasn't actually fixed, so if we were to commit the// suspended fallback, it would enter an endless error recovery loop.
//// Instead, we disable error recovery for these lanes and start// over again.
// This time, the error is thrown and we commit the result.
'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 " />});// Higher priority update that interrupts the first renderReactNoop.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>,);});});// Even though the initial render was a transition, it shows a fallback.
assertLog(['Async text requested [Async]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
// Resolve the original dataawait act(() => {resolveTextRequests('Async');});// During the retry, a fresh request is initiated. Now we must wait for this
// one to finish.
// TODO: This is awkward. Intuitively, you might expect for `act` to wait
// until the new request has finished loading. But if it's mock IO, as in
// this test, how would the developer be able to imperatively flush it if it// wasn't initiated until the current `act` call? Can't think of a better
// strategy at the moment.
assertLog(['Async text requested [Async]']);
expect(root).toMatchRenderedOutput('Loading...');
// Flush the second request.
await act(() => {resolveTextRequests('Async');});// This time it finishes because it was during a retry.
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..." />} />);
});// Start a transition on one root. It will suspend.
await act(() => {startTransition(() => {root1.render(
<Suspense fallback={<Text text="Loading..." />}>
<App /></Suspense>,);});});assertLog(['Async text requested [Hi]']);
// While we're waiting for the first root's data to resolve, a second// root renders.
const root2 = ReactNoop.createRoot();
await act(() => {root2.render('Do re mi');
});expect(root2).toMatchRenderedOutput('Do re mi');
// Once the first root's data is ready, we should finish its transition.
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]']);
// Calling a hook should error because we're oustide of a component.
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) {// This thenable immediately resolves, synchronously, without waiting// a microtask.
resolve('Hi');},};try {return <Text text={use(thenable)} />;} catch {throw new Error('`use` should not suspend because the thenable resolved synchronously.',
);}}// Because the thenable resolves synchronously, we should be able to finish// rendering synchronously, with no fallback.
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 (<Texttext={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 () => {// Perform an update that causes the app to suspendstartTransition(() => {setChildShouldSuspend(true);});await waitFor(['childShouldSuspend: true, showChild: true']);
// While the update is in progress, schedule another update.
startTransition(() => {setShowChild(false);});});assertLog([
// Because the interleaved update is not higher priority than what we were// already working on, it won't interrupt. The first update will continue,// and will suspend.'Async text requested [Will never resolve]',
// Instead of waiting for the promise to resolve, React notices there's// another pending update that it hasn't tried yet. It will switch to
// rendering that instead.
//// This time, the update hides the component that previous was suspending,// so it finishes successfully.
'childShouldSuspend: false, showChild: false','(empty)',// Finally, React attempts to render the first update again. It also
// finishes successfully, because it was rebased on top of the update that// hid the suspended component.
// NOTE: These this render happened to not be entangled with the previous// one. If they had been, this update would have been included in the
// previous render, and there wouldn't be an extra one here. This could
// change if we change our entanglement heurstics. Semantically, it
// shouldn't matter, though in general we try to work on transitions in// parallel whenever possible. So even though in this particular case, the
// extra render is unnecessary, it's a nice property that it wasn't// entangled with the other transition.
'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}) {// This computes the uppercased version of some text. Pretend it's an
// expensive operation that we want to reuse.
const uppercaseText = useMemo(() => {Scheduler.log('Compute uppercase: ' + text);
return text.toUpperCase();
}, [text]);
// This adds an exclamation point to the text. Pretend it's an async
// operation that is sent to a service for processing.
const exclamatoryText = use(getAsyncText(uppercaseText + '!'));
// This surrounds the text with sparkle emojis. The purpose in this test
// is to show that you can suspend in the middle of a sequence of hooks// without breaking anything.
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" />);
});});// Suspends while we wait for the async service to respond.
assertLog(['Compute uppercase: Hello', 'Async text requested [HELLO!]']);
expect(root).toMatchRenderedOutput(null);
// The data is received.
await act(() => {resolveTextRequests('HELLO!');});assertLog([
// We shouldn't run the uppercase computation again, because we can reuse// the computation from the previous attempt.// 'Compute uppercase: Hello','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} />;
}// Initial render.
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');
// Update the state variable after the use().
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');
// Update the state variable before the use(). The second state is maintained.
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 () => {
// Make sure we don't get a Hook mismatch warning on updates if there were non-stateful Hooks before the use().
let _setLawyer;function Lexicon() {useDebugValue(123);const avocado = use(getAsyncText('aguacate'));const [lawyer, setLawyer] = useState('abogado');
_setLawyer = setLawyer;return <Text text={avocado + ' ' + lawyer} />;
}// Initial render.
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');
// Now update the state.
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([
// We shouldn't request async text again, because the async function// was memoized// 'Async text requested [Hello]'
'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]', '(Loading A...)']);
expect(root).toMatchRenderedOutput('(Loading A...)');
await act(() => {resolveTextRequests('A');});assertLog(['A', 'Async text requested [B]', '(Loading B...)']);
expect(root).toMatchRenderedOutput('A(Loading B...)');
await act(() => {resolveTextRequests('B');});assertLog(['B', 'Async text requested [C]', '(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 () => {// This the same as the previous test, except the requests are not cached.
// The tree should still eventually resolve, despite the// duplicate requests.
function AsyncText({text}) {// This initiates a new request on each render.
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]', '(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([
// React suspends until A finishes loading.'Async text requested [A]',
'A',// Now React can continue rendering the rest of the tree.
// React does not suspend on the inner requests, because that would// block A from appearing. Instead it shows a fallback.
'Async text requested [B]',
'(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([
// React suspends until B finishes loading.'Async text requested [B]',
'B',// React does not suspend on C, because that would block B from appearing.
'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([
// TODO: We shouldn't have to replay the function body again. Skip// straight to reconciliation.'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([
// TODO: We shouldn't have to replay the render function again. We could// skip straight to reconciliation. However, it's not as urgent to fix// this for fiber types that aren't function components, so we can special// case those in the meantime.'Async text requested [Hi]',
'Hi',]);expect(root).toMatchRenderedOutput('Hi');
});test('async children are recursively unwrapped', async () => {// This is a Usable of a Usable. `use` would only unwrap a single level, but
// when passed as a child, the reconciler recurisvely unwraps until it// resolves to a non-Usable value.
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}) {// The child returned by this component is always a promise (async// functions always return promises). React should unwrap it and reconcile
// the result, not the promise itself.
return <Child text={text} />;}const root = ReactNoop.createRoot();
await act(() => {startTransition(() => {root.render(<App text="A" />);
});});assertLog(['A', 'Mount: A']);
expect(root).toMatchRenderedOutput('A');
// Update the child's props. It should not remount.
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" />)}
</>,);});});// Nothing should have remountedassertLog(['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" />)}
</>,);});});// Nothing should have remountedassertLog(['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>
);}// Initial renderconst root = ReactNoop.createRoot();
await act(() => {root.render(<App contextValue={<Child text="A" />} />);
});assertLog(['Indirection', 'ParentOfContextNode', 'A', 'Mount']);
expect(root).toMatchRenderedOutput('A');
// Update the child to a new valueawait act(async () => {root.render(<App contextValue={<Child text="B" />} />);
});assertLog([
// Notice that the <Indirection /> did not rerender, because the// update was sent via Context.// TODO: We shouldn't have to re-render the parent of the context node.// This happens because we need to reconcile the parent's children again.// However, we should be able to skip directly to reconcilation without// evaluating the component. One way to do this might be to mark the// context dependency with a flag that says it was added// during reconcilation.'ParentOfContextNode',// Notice that this was an update, not a remount.'B',]);
expect(root).toMatchRenderedOutput('B');
// Delete the old child and replace it with a new one, by changing the keyawait act(async () => {root.render(<App contextValue={<Child key="C" text="C" />} />);
});assertLog([
'ParentOfContextNode',// A new instance is mounted'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');
});test('unwrap uncached promises inside forwardRef', async () => {const asyncInstance = {};const Async = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => asyncInstance);
const text = use(Promise.resolve('Async'));
return <Text text={text} />;});const ref = React.createRef();
function App() {return (<Suspense fallback={<Text text="Loading..." />}>
<Async ref={ref} /></Suspense>);}const root = ReactNoop.createRoot();
await act(() => {startTransition(() => {root.render(<App />);
});});assertLog(['Async']);
expect(root).toMatchRenderedOutput('Async');
expect(ref.current).toBe(asyncInstance);
});test('unwrap uncached promises inside memo', async () => {const Async = React.memo(
props => {const text = use(Promise.resolve(props.text));
return <Text text={text} />;},(a, b) => a.text === b.text,
);function App({text}) {return (<Suspense fallback={<Text text="Loading..." />}>
<Async text={text} /></Suspense>);}const root = ReactNoop.createRoot();
await act(() => {startTransition(() => {root.render(<App text="Async" />);
});});assertLog(['Async']);
expect(root).toMatchRenderedOutput('Async');
// Update to the same valueawait act(() => {startTransition(() => {root.render(<App text="Async" />);
});});// Should not have re-rendered, because it's memoizedassertLog([]);
expect(root).toMatchRenderedOutput('Async');
// Update to a different valueawait act(() => {startTransition(() => {root.render(<App text="Async!" />);
});});assertLog(['Async!']);
expect(root).toMatchRenderedOutput('Async!');
});// @gate !disableLegacyContext
test('unwrap uncached promises in component that accesses legacy context', async () => {
class ContextProvider extends React.Component {
static childContextTypes = {
legacyContext() {},
};
getChildContext() {
return {legacyContext: 'Async'};
}render() {
return this.props.children;
}}function Async({label}, context) {
const text = use(Promise.resolve(context.legacyContext + ` (${label})`));
return <Text text={text} />;
}Async.contextTypes = {
legacyContext: () => {},
};const AsyncMemo = React.memo(Async, (a, b) => a.label === b.label);
function App() {
return (
<ContextProvider>
<Suspense fallback={<Text text="Loading..." />}>
<div>
<Async label="function component" />
</div>
<div><AsyncMemo label="memo component" />
</div>
</Suspense>
</ContextProvider>
);}const root = ReactNoop.createRoot();
await act(() => {startTransition(() => {root.render(<App />);
});});assertLog(['Async (function component)', 'Async (memo component)']);
expect(root).toMatchRenderedOutput(
<><div>Async (function component)</div><div>Async (memo component)</div></>,);});test('regression test: updates while component is suspended should not be mistaken for render phase updates', async () => {const getCachedAsyncText = cache(getAsyncText);let setState;function App() {const [state, _setState] = useState('A');
setState = _setState;return <Text text={use(getCachedAsyncText(state))} />;}// Initial renderconst root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog(['Async text requested [A]']);
expect(root).toMatchRenderedOutput(null);
await act(() => resolveTextRequests('A'));assertLog(['A']);
expect(root).toMatchRenderedOutput('A');
// Update to B. This will suspend.
await act(() => startTransition(() => setState('B')));assertLog(['Async text requested [B]']);
expect(root).toMatchRenderedOutput('A');
// While B is suspended, update to C. This should immediately interrupt
// the render for B. In the regression, this update was mistakenly treated
// as a render phase update.
ReactNoop.flushSync(() => setState('C'));
assertLog(['Async text requested [C]']);
// Finish rendering.
await act(() => resolveTextRequests('C'));assertLog(['C']);
expect(root).toMatchRenderedOutput('C');
});// @gate !forceConcurrentByDefaultForTesting
test('an async component outside of a Suspense boundary crashes with an error (resolves in microtask)', 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;
}}async function AsyncClientComponent() {
return <Text text="Hi" />;
}const root = ReactNoop.createRoot();
await expect(async () => {
await act(() => {
root.render(
<ErrorBoundary>
<AsyncClientComponent />
</ErrorBoundary>,
);});}).toErrorDev([
'async/await is not yet supported in Client Components, only ' +'Server Components. This error is often caused by accidentally ' +"adding `'use client'` to a module that was originally written " +'for the server.',]);
assertLog([
'async/await is not yet supported in Client Components, only Server ' +'Components. This error is often caused by accidentally adding ' +"`'use client'` to a module that was originally written for " +'the server.','async/await is not yet supported in Client Components, only Server ' +'Components. This error is often caused by accidentally adding ' +"`'use client'` to a module that was originally written for " +'the server.',]);
expect(root).toMatchRenderedOutput(
'async/await is not yet supported in Client Components, only Server ' +
'Components. This error is often caused by accidentally adding ' +
"`'use client'` to a module that was originally written for " +
'the server.',
);});// @gate !forceConcurrentByDefaultForTesting
test('an async component outside of a Suspense boundary crashes with an error (resolves in macrotask)', 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;
}}async function AsyncClientComponent() {
await waitForMicrotasks();
return <Text text="Hi" />;
}const root = ReactNoop.createRoot();
await expect(async () => {
await act(() => {
root.render(
<ErrorBoundary>
<AsyncClientComponent />
</ErrorBoundary>,
);});}).toErrorDev([
'async/await is not yet supported in Client Components, only ' +'Server Components. This error is often caused by accidentally ' +"adding `'use client'` to a module that was originally written " +'for the server.',]);
assertLog([
'async/await is not yet supported in Client Components, only Server ' +'Components. This error is often caused by accidentally adding ' +"`'use client'` to a module that was originally written for " +'the server.','async/await is not yet supported in Client Components, only Server ' +'Components. This error is often caused by accidentally adding ' +"`'use client'` to a module that was originally written for " +'the server.',]);
expect(root).toMatchRenderedOutput(
'async/await is not yet supported in Client Components, only Server ' +
'Components. This error is often caused by accidentally adding ' +
"`'use client'` to a module that was originally written for " +
'the server.',
);});test('warn if async client component calls a hook (e.g. useState)', async () => {
async function AsyncClientComponent() {useState();return <Text text="Hi" />;}const root = ReactNoop.createRoot();
await expect(async () => {await act(() => {startTransition(() => {root.render(<AsyncClientComponent />);
});});}).toErrorDev([
'Hooks are not supported inside an async component. This ' +"error is often caused by accidentally adding `'use client'` " +'to a module that was originally written for the server.',]);
});test('warn if async client component calls a hook (e.g. use)', async () => {
const promise = Promise.resolve();
async function AsyncClientComponent() {use(promise);return <Text text="Hi" />;}const root = ReactNoop.createRoot();
await expect(async () => {await act(() => {startTransition(() => {root.render(<AsyncClientComponent />);
});});}).toErrorDev([
'Hooks are not supported inside an async component. This ' +"error is often caused by accidentally adding `'use client'` " +'to a module that was originally written for the server.',]);
});});