diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 74b17db938cce..7071ef5081542 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -2170,7 +2170,7 @@ describe('ReactFlight', () => {
);
});
- // @gate enableFlightReadableStream
+ // @gate enableFlightReadableStream && enableAsyncIterableChildren
it('shares state when moving keyed Server Components that render async iterables', async () => {
function StatefulClient({name, initial}) {
const [state] = React.useState(initial);
@@ -2183,39 +2183,11 @@ describe('ReactFlight', () => {
yield ;
}
- function ListClient({children}) {
- // TODO: Unwrap AsyncIterables natively in React. For now we do it in this wrapper.
- const resolvedChildren = [];
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
- for (const fragment of children) {
- // We should've wrapped each child in a keyed Fragment.
- expect(fragment.type).toBe(React.Fragment);
- const fragmentChildren = [];
- const iterator = fragment.props.children[Symbol.asyncIterator]();
- if (iterator === fragment.props.children) {
- console.error(
- 'AyncIterators are not valid children of React. It must be a multi-shot AsyncIterable.',
- );
- }
- for (let entry; !(entry = React.use(iterator.next())).done; ) {
- fragmentChildren.push(entry.value);
- }
- resolvedChildren.push(
-
- {fragmentChildren}
- ,
- );
- }
- return
{resolvedChildren}
;
- }
-
- const List = clientReference(ListClient);
-
const transport = ReactNoopFlightServer.render(
-
+
- ,
+
,
);
await act(async () => {
@@ -2234,10 +2206,10 @@ describe('ReactFlight', () => {
// We swap the Server Components and the state of each child inside each fragment should move.
// Really the Fragment itself moves.
const transport2 = ReactNoopFlightServer.render(
-
+
- ,
+
,
);
await act(async () => {
@@ -2336,7 +2308,7 @@ describe('ReactFlight', () => {
);
});
- // @gate enableFlightReadableStream
+ // @gate enableFlightReadableStream && enableAsyncIterableChildren
it('preserves debug info for server-to-server pass through of async iterables', async () => {
let resolve;
const iteratorPromise = new Promise(r => (resolve = r));
@@ -2347,23 +2319,6 @@ describe('ReactFlight', () => {
resolve();
}
- function ListClient({children: fragment}) {
- // TODO: Unwrap AsyncIterables natively in React. For now we do it in this wrapper.
- const resolvedChildren = [];
- const iterator = fragment.props.children[Symbol.asyncIterator]();
- if (iterator === fragment.props.children) {
- console.error(
- 'AyncIterators are not valid children of React. It must be a multi-shot AsyncIterable.',
- );
- }
- for (let entry; !(entry = React.use(iterator.next())).done; ) {
- resolvedChildren.push(entry.value);
- }
- return {resolvedChildren}
;
- }
-
- const List = clientReference(ListClient);
-
function Keyed({children}) {
// Keying this should generate a fragment.
return children;
@@ -2375,9 +2330,9 @@ describe('ReactFlight', () => {
ReactNoopFlightClient.read(transport),
).root;
return (
-
+
{children}
-
+
);
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 64964031d59ed..dc514f2939ed6 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -3346,7 +3346,7 @@ describe('ReactDOMFizzServer', () => {
]);
});
- it('Supports iterable', async () => {
+ it('supports iterable', async () => {
const Immutable = require('immutable');
const mappedJSX = Immutable.fromJS([
@@ -3366,7 +3366,71 @@ describe('ReactDOMFizzServer', () => {
);
});
- it('Supports bigint', async () => {
+ // @gate enableAsyncIterableChildren
+ it('supports async generator component', async () => {
+ async function* App() {
+ yield {await Promise.resolve('Hi')};
+ yield ' ';
+ yield {await Promise.resolve('World')};
+ }
+
+ await act(async () => {
+ const {pipe} = renderToPipeableStream(
+ ,
+ );
+ pipe(writable);
+ });
+
+ // Each act retries once which causes a new ping which schedules
+ // new work but only after the act has finished rendering.
+ await act(() => {});
+ await act(() => {});
+ await act(() => {});
+ await act(() => {});
+
+ expect(getVisibleChildren(container)).toEqual(
+
+ Hi World
+
,
+ );
+ });
+
+ // @gate enableAsyncIterableChildren
+ it('supports async iterable children', async () => {
+ const iterable = {
+ async *[Symbol.asyncIterator]() {
+ yield {await Promise.resolve('Hi')};
+ yield ' ';
+ yield {await Promise.resolve('World')};
+ },
+ };
+
+ function App({children}) {
+ return {children}
;
+ }
+
+ await act(() => {
+ const {pipe} = renderToPipeableStream({iterable});
+ pipe(writable);
+ });
+
+ // Each act retries once which causes a new ping which schedules
+ // new work but only after the act has finished rendering.
+ await act(() => {});
+ await act(() => {});
+ await act(() => {});
+ await act(() => {});
+
+ expect(getVisibleChildren(container)).toEqual(
+
+ Hi World
+
,
+ );
+ });
+
+ it('supports bigint', async () => {
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
{10n}
,
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index b11e8b59ad1eb..e2dfe949a71b5 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -27,6 +27,7 @@ import {
} from './ReactFiberFlags';
import {
getIteratorFn,
+ ASYNC_ITERATOR,
REACT_ELEMENT_TYPE,
REACT_FRAGMENT_TYPE,
REACT_PORTAL_TYPE,
@@ -42,7 +43,10 @@ import {
FunctionComponent,
} from './ReactWorkTags';
import isArray from 'shared/isArray';
-import {enableRefAsProp} from 'shared/ReactFeatureFlags';
+import {
+ enableRefAsProp,
+ enableAsyncIterableChildren,
+} from 'shared/ReactFeatureFlags';
import {
createWorkInProgress,
@@ -587,7 +591,12 @@ function createChildReconciler(
}
}
- if (isArray(newChild) || getIteratorFn(newChild)) {
+ if (
+ isArray(newChild) ||
+ getIteratorFn(newChild) ||
+ (enableAsyncIterableChildren &&
+ typeof newChild[ASYNC_ITERATOR] === 'function')
+ ) {
const created = createFiberFromFragment(
newChild,
returnFiber.mode,
@@ -711,7 +720,12 @@ function createChildReconciler(
}
}
- if (isArray(newChild) || getIteratorFn(newChild)) {
+ if (
+ isArray(newChild) ||
+ getIteratorFn(newChild) ||
+ (enableAsyncIterableChildren &&
+ typeof newChild[ASYNC_ITERATOR] === 'function')
+ ) {
if (key !== null) {
return null;
}
@@ -833,7 +847,12 @@ function createChildReconciler(
);
}
- if (isArray(newChild) || getIteratorFn(newChild)) {
+ if (
+ isArray(newChild) ||
+ getIteratorFn(newChild) ||
+ (enableAsyncIterableChildren &&
+ typeof newChild[ASYNC_ITERATOR] === 'function')
+ ) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(
returnFiber,
@@ -1112,7 +1131,7 @@ function createChildReconciler(
return resultingFirstChild;
}
- function reconcileChildrenIterator(
+ function reconcileChildrenIteratable(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable,
@@ -1171,6 +1190,80 @@ function createChildReconciler(
}
}
+ return reconcileChildrenIterator(
+ returnFiber,
+ currentFirstChild,
+ newChildren,
+ lanes,
+ debugInfo,
+ );
+ }
+
+ function reconcileChildrenAsyncIteratable(
+ returnFiber: Fiber,
+ currentFirstChild: Fiber | null,
+ newChildrenIterable: AsyncIterable,
+ lanes: Lanes,
+ debugInfo: ReactDebugInfo | null,
+ ): Fiber | null {
+ const newChildren = newChildrenIterable[ASYNC_ITERATOR]();
+
+ if (__DEV__) {
+ if (newChildren === newChildrenIterable) {
+ // We don't support rendering AsyncGenerators as props because it's a mutation.
+ // We do support generators if they were created by a AsyncGeneratorFunction component
+ // as its direct child since we can recreate those by rerendering the component
+ // as needed.
+ const isGeneratorComponent =
+ returnFiber.tag === FunctionComponent &&
+ // $FlowFixMe[method-unbinding]
+ Object.prototype.toString.call(returnFiber.type) ===
+ '[object AsyncGeneratorFunction]' &&
+ // $FlowFixMe[method-unbinding]
+ Object.prototype.toString.call(newChildren) ===
+ '[object AsyncGenerator]';
+ if (!isGeneratorComponent) {
+ if (!didWarnAboutGenerators) {
+ console.error(
+ 'Using AsyncIterators as children is unsupported and will likely yield ' +
+ 'unexpected results because enumerating a generator mutates it. ' +
+ 'You can use an AsyncIterable that can iterate multiple times over ' +
+ 'the same items.',
+ );
+ }
+ didWarnAboutGenerators = true;
+ }
+ }
+ }
+
+ if (newChildren == null) {
+ throw new Error('An iterable object provided no iterator.');
+ }
+
+ // To save bytes, we reuse the logic by creating a synchronous Iterable and
+ // reusing that code path.
+ const iterator: Iterator = ({
+ next(): IteratorResult {
+ return unwrapThenable(newChildren.next());
+ },
+ }: any);
+
+ return reconcileChildrenIterator(
+ returnFiber,
+ currentFirstChild,
+ iterator,
+ lanes,
+ debugInfo,
+ );
+ }
+
+ function reconcileChildrenIterator(
+ returnFiber: Fiber,
+ currentFirstChild: Fiber | null,
+ newChildren: ?Iterator,
+ lanes: Lanes,
+ debugInfo: ReactDebugInfo | null,
+ ): Fiber | null {
if (newChildren == null) {
throw new Error('An iterable object provided no iterator.');
}
@@ -1563,7 +1656,20 @@ function createChildReconciler(
}
if (getIteratorFn(newChild)) {
- return reconcileChildrenIterator(
+ return reconcileChildrenIteratable(
+ returnFiber,
+ currentFirstChild,
+ newChild,
+ lanes,
+ mergeDebugInfo(debugInfo, newChild._debugInfo),
+ );
+ }
+
+ if (
+ enableAsyncIterableChildren &&
+ typeof newChild[ASYNC_ITERATOR] === 'function'
+ ) {
+ return reconcileChildrenAsyncIteratable(
returnFiber,
currentFirstChild,
newChild,
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index bad664adbc1a1..59e7ae780f20a 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -414,7 +414,10 @@ function warnIfAsyncClientComponent(Component: Function) {
// bulletproof but together they cover the most common cases.
const isAsyncFunction =
// $FlowIgnore[method-unbinding]
- Object.prototype.toString.call(Component) === '[object AsyncFunction]';
+ Object.prototype.toString.call(Component) === '[object AsyncFunction]' ||
+ // $FlowIgnore[method-unbinding]
+ Object.prototype.toString.call(Component) ===
+ '[object AsyncGeneratorFunction]';
if (isAsyncFunction) {
// Encountered an async Client Component. This is not yet supported.
const componentName = getComponentNameFromFiber(currentlyRenderingFiber);
diff --git a/packages/react-reconciler/src/__tests__/ReactUse-test.js b/packages/react-reconciler/src/__tests__/ReactUse-test.js
index 467c68ff52753..dede68854c615 100644
--- a/packages/react-reconciler/src/__tests__/ReactUse-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactUse-test.js
@@ -1,3 +1,12 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
'use strict';
let React;
@@ -1816,4 +1825,94 @@ describe('ReactUse', () => {
'supported, except via a Suspense-compatible library or framework.',
]);
});
+
+ // @gate enableAsyncIterableChildren
+ test('async generator component', async () => {
+ let hi, world;
+ async function* App() {
+ // Only cached promises can be awaited in async generators because
+ // when we rerender, it'll issue another request which blocks the next.
+ await (hi || (hi = getAsyncText('Hi')));
+ yield ;
+ yield ' ';
+ await (world || (world = getAsyncText('World')));
+ yield ;
+ }
+
+ const root = ReactNoop.createRoot();
+ await expect(async () => {
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+ }).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 text requested [Hi]']);
+
+ await expect(async () => {
+ await act(() => resolveTextRequests('Hi'));
+ }).toErrorDev(
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. Creating ' +
+ 'promises inside a Client Component or hook is not yet ' +
+ 'supported, except via a Suspense-compatible library or framework.',
+ );
+
+ assertLog(['Async text requested [World]']);
+
+ await act(() => resolveTextRequests('World'));
+
+ assertLog(['Hi', 'World']);
+ expect(root).toMatchRenderedOutput('Hi World');
+ });
+
+ // @gate enableAsyncIterableChildren
+ test('async iterable children', async () => {
+ let hi, world;
+ const iterable = {
+ async *[Symbol.asyncIterator]() {
+ // Only cached promises can be awaited in async iterables because
+ // when we retry, it'll ask for another iterator which issues another
+ // request which blocks the next.
+ await (hi || (hi = getAsyncText('Hi')));
+ yield ;
+ yield ' ';
+ await (world || (world = getAsyncText('World')));
+ yield ;
+ },
+ };
+
+ function App({children}) {
+ return {children}
;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ startTransition(() => {
+ root.render({iterable});
+ });
+ });
+ assertLog(['Async text requested [Hi]']);
+
+ await expect(async () => {
+ await act(() => resolveTextRequests('Hi'));
+ }).toErrorDev(
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. Creating ' +
+ 'promises inside a Client Component or hook is not yet ' +
+ 'supported, except via a Suspense-compatible library or framework.',
+ );
+
+ assertLog(['Async text requested [World]']);
+
+ await act(() => resolveTextRequests('World'));
+
+ assertLog(['Hi', 'World']);
+ expect(root).toMatchRenderedOutput(Hi World
);
+ });
});
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 5caff3ca14751..05cc1a2e1bead 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -25,7 +25,11 @@ import type {TransitionStatus} from './ReactFizzConfig';
import {readContext as readContextImpl} from './ReactFizzNewContext';
import {getTreeId} from './ReactFizzTreeContext';
-import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
+import {
+ createThenableState,
+ trackUsedThenable,
+ readPreviousThenable,
+} from './ReactFizzThenable';
import {makeId, NotPendingTransition} from './ReactFizzConfig';
import {createFastHash} from './ReactServerStreamConfig';
@@ -229,6 +233,13 @@ export function prepareToUseHooks(
thenableState = prevThenableState;
}
+export function prepareToUseThenableState(
+ prevThenableState: ThenableState | null,
+): void {
+ thenableIndexCounter = 0;
+ thenableState = prevThenableState;
+}
+
export function finishHooks(
Component: any,
props: any,
@@ -765,6 +776,15 @@ export function unwrapThenable(thenable: Thenable): T {
return trackUsedThenable(thenableState, thenable, index);
}
+export function readPreviousThenableFromState(): T | void {
+ const index = thenableIndexCounter;
+ thenableIndexCounter += 1;
+ if (thenableState === null) {
+ return undefined;
+ }
+ return readPreviousThenable(thenableState, index);
+}
+
function unsupportedRefresh() {
throw new Error('Cache cannot be refreshed during server rendering.');
}
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 00ed4d8f06f6d..b6925b1c7b58b 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -98,6 +98,7 @@ import {
} from './ReactFizzNewContext';
import {
prepareToUseHooks,
+ prepareToUseThenableState,
finishHooks,
checkDidRenderIdHook,
resetHooksState,
@@ -106,6 +107,7 @@ import {
setCurrentResumableState,
getThenableStateAfterSuspending,
unwrapThenable,
+ readPreviousThenableFromState,
getActionStateCount,
getActionStateMatchingIndex,
} from './ReactFizzHooks';
@@ -115,6 +117,7 @@ import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
import {
getIteratorFn,
+ ASYNC_ITERATOR,
REACT_ELEMENT_TYPE,
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
@@ -144,6 +147,7 @@ import {
enableRenderableContext,
enableRefAsProp,
disableDefaultPropsExceptForClasses,
+ enableAsyncIterableChildren,
} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
@@ -2165,6 +2169,7 @@ function validateIterable(
// as its direct child since we can recreate those by rerendering the component
// as needed.
const isGeneratorComponent =
+ childIndex === -1 && // Only the root child is valid
task.componentStack !== null &&
task.componentStack.tag === 1 && // FunctionComponent
// $FlowFixMe[method-unbinding]
@@ -2197,6 +2202,43 @@ function validateIterable(
}
}
+function validateAsyncIterable(
+ task: Task,
+ iterable: AsyncIterable,
+ childIndex: number,
+ iterator: AsyncIterator,
+): void {
+ if (__DEV__) {
+ if (iterator === iterable) {
+ // We don't support rendering Generators as props because it's a mutation.
+ // See https://github.com/facebook/react/issues/12995
+ // We do support generators if they were created by a GeneratorFunction component
+ // as its direct child since we can recreate those by rerendering the component
+ // as needed.
+ const isGeneratorComponent =
+ childIndex === -1 && // Only the root child is valid
+ task.componentStack !== null &&
+ task.componentStack.tag === 1 && // FunctionComponent
+ // $FlowFixMe[method-unbinding]
+ Object.prototype.toString.call(task.componentStack.type) ===
+ '[object AsyncGeneratorFunction]' &&
+ // $FlowFixMe[method-unbinding]
+ Object.prototype.toString.call(iterator) === '[object AsyncGenerator]';
+ if (!isGeneratorComponent) {
+ if (!didWarnAboutGenerators) {
+ console.error(
+ 'Using AsyncIterators as children is unsupported and will likely yield ' +
+ 'unexpected results because enumerating a generator mutates it. ' +
+ 'You can use an AsyncIterable that can iterate multiple times over ' +
+ 'the same items.',
+ );
+ }
+ didWarnAboutGenerators = true;
+ }
+ }
+ }
+}
+
function warnOnFunctionType(invalidChild: Function) {
if (__DEV__) {
const name = invalidChild.displayName || invalidChild.name || 'Component';
@@ -2327,7 +2369,6 @@ function renderNodeDestructive(
// TODO: This is not great but I think it's inherent to the id
// generation algorithm.
let step = iterator.next();
- // If there are not entries, we need to push an empty so we start by checking that.
if (!step.done) {
const children = [];
do {
@@ -2335,12 +2376,76 @@ function renderNodeDestructive(
step = iterator.next();
} while (!step.done);
renderChildrenArray(request, task, children, childIndex);
- return;
}
return;
}
}
+ if (
+ enableAsyncIterableChildren &&
+ typeof (node: any)[ASYNC_ITERATOR] === 'function'
+ ) {
+ const iterator: AsyncIterator = (node: any)[
+ ASYNC_ITERATOR
+ ]();
+ if (iterator) {
+ if (__DEV__) {
+ validateAsyncIterable(task, (node: any), childIndex, iterator);
+ }
+ // TODO: Update the task.node to be the iterator to avoid asking
+ // for new iterators, but we currently warn for rendering these
+ // so needs some refactoring to deal with the warning.
+
+ // We need to push a component stack because if this suspends, we'll pop a stack.
+ const previousComponentStack = task.componentStack;
+ task.componentStack = createBuiltInComponentStack(
+ task,
+ 'AsyncIterable',
+ );
+
+ // Restore the thenable state before resuming.
+ const prevThenableState = task.thenableState;
+ task.thenableState = null;
+ prepareToUseThenableState(prevThenableState);
+
+ // We need to know how many total children are in this set, so that we
+ // can allocate enough id slots to acommodate them. So we must exhaust
+ // the iterator before we start recursively rendering the children.
+ // TODO: This is not great but I think it's inherent to the id
+ // generation algorithm.
+ const children = [];
+
+ let done = false;
+
+ if (iterator === node) {
+ // If it's an iterator we need to continue reading where we left
+ // off. We can do that by reading the first few rows from the previous
+ // thenable state.
+ // $FlowFixMe
+ let step = readPreviousThenableFromState();
+ while (step !== undefined) {
+ if (step.done) {
+ done = true;
+ break;
+ }
+ children.push(step.value);
+ step = readPreviousThenableFromState();
+ }
+ }
+
+ if (!done) {
+ let step = unwrapThenable(iterator.next());
+ while (!step.done) {
+ children.push(step.value);
+ step = unwrapThenable(iterator.next());
+ }
+ }
+ task.componentStack = previousComponentStack;
+ renderChildrenArray(request, task, children, childIndex);
+ return;
+ }
+ }
+
// Usables are a valid React node type. When React encounters a Usable in
// a child position, it unwraps it using the same algorithm as `use`. For
// example, for promises, React will throw an exception to unwind the
@@ -3554,6 +3659,11 @@ function retryRenderTask(
const ping = task.ping;
x.then(ping, ping);
task.thenableState = getThenableStateAfterSuspending();
+ // We pop one task off the stack because the node that suspended will be tried again,
+ // which will add it back onto the stack.
+ if (task.componentStack !== null) {
+ task.componentStack = task.componentStack.parent;
+ }
return;
} else if (
enablePostpone &&
@@ -3639,6 +3749,11 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
const ping = task.ping;
x.then(ping, ping);
task.thenableState = getThenableStateAfterSuspending();
+ // We pop one task off the stack because the node that suspended will be tried again,
+ // which will add it back onto the stack.
+ if (task.componentStack !== null) {
+ task.componentStack = task.componentStack.parent;
+ }
return;
}
}
diff --git a/packages/react-server/src/ReactFizzThenable.js b/packages/react-server/src/ReactFizzThenable.js
index 1991b9de1ca61..1494b4188e024 100644
--- a/packages/react-server/src/ReactFizzThenable.js
+++ b/packages/react-server/src/ReactFizzThenable.js
@@ -131,6 +131,19 @@ export function trackUsedThenable(
}
}
+export function readPreviousThenable(
+ thenableState: ThenableState,
+ index: number,
+): void | T {
+ const previous = thenableState[index];
+ if (previous === undefined) {
+ return undefined;
+ } else {
+ // We assume this has been resolved already.
+ return (previous: any).value;
+ }
+}
+
// This is used to track the actual thenable that suspended so it can be
// passed to the rest of the Suspense implementation — which, for historical
// reasons, expects to receive a thenable.
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 545afd1cdcc32..25b93907cfa64 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -82,6 +82,7 @@ export const enableFetchInstrumentation = true;
export const enableBinaryFlight = __EXPERIMENTAL__;
export const enableFlightReadableStream = __EXPERIMENTAL__;
+export const enableAsyncIterableChildren = __EXPERIMENTAL__;
export const enableTaint = __EXPERIMENTAL__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index a4d0434f50469..52c6f79d967c6 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -46,6 +46,7 @@ export const enableLegacyCache = false;
export const enableFetchInstrumentation = false;
export const enableBinaryFlight = true;
export const enableFlightReadableStream = true;
+export const enableAsyncIterableChildren = false;
export const enableTaint = true;
export const enablePostpone = false;
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 723a6a69c376b..03c733dbc9411 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -103,6 +103,7 @@ export const enableTransitionTracing = false;
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
export const passChildrenWhenCloningPersistedNodes = false;
export const enableEarlyReturnForPropDiffing = false;
+export const enableAsyncIterableChildren = false;
export const renameElementSymbol = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 2271d9b2a25db..565f257914987 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -23,6 +23,7 @@ export const enableLegacyCache = __EXPERIMENTAL__;
export const enableFetchInstrumentation = true;
export const enableBinaryFlight = true;
export const enableFlightReadableStream = true;
+export const enableAsyncIterableChildren = false;
export const enableTaint = true;
export const enablePostpone = false;
export const disableCommentsAsDOMContainers = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index e6b299035a99e..59f7ecef41733 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -23,6 +23,7 @@ export const enableLegacyCache = false;
export const enableFetchInstrumentation = false;
export const enableBinaryFlight = true;
export const enableFlightReadableStream = true;
+export const enableAsyncIterableChildren = false;
export const enableTaint = true;
export const enablePostpone = false;
export const disableCommentsAsDOMContainers = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 44972804eb2de..6ba061962456d 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -23,6 +23,7 @@ export const enableLegacyCache = true;
export const enableFetchInstrumentation = false;
export const enableBinaryFlight = true;
export const enableFlightReadableStream = true;
+export const enableAsyncIterableChildren = false;
export const enableTaint = true;
export const enablePostpone = false;
export const disableCommentsAsDOMContainers = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index cf634cb168700..4e252d59d6801 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -73,6 +73,7 @@ export const enableFetchInstrumentation = false;
export const enableBinaryFlight = false;
export const enableFlightReadableStream = false;
+export const enableAsyncIterableChildren = false;
export const enableTaint = false;