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;