From 9f2eebd807bf53b7d9901cf0b768762948224cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 22 Apr 2024 13:25:05 -0400 Subject: [PATCH] [Fiber/Fizz] Support AsyncIterable as Children and AsyncGenerator Client Components (#28868) Stacked on #28849, #28854, #28853. Behind a flag. If you're following along from the side-lines. This is probably not what you think it is. It's NOT a way to get updates to a component over time. The AsyncIterable works like an Iterable already works in React which is how an Array works. I.e. it's a list of children - not the value of a child over time. It also doesn't actually render one component at a time. The way it works is more like awaiting the entire list to become an array and then it shows up. Before that it suspends the parent. To actually get these to display one at a time, you have to opt-in with `` to describe how they should appear. That's really the interesting part and that not implemented yet. Additionally, since these are effectively Async Functions and uncached promises, they're not actually fully "supported" on the client yet for the same reason rendering plain Promises and Async Functions aren't. They warn. It's only really useful when paired with RSC that produces instrumented versions of these. Ideally we'd published instrumented helpers to help with map/filter style operations that yield new instrumented AsyncIterables. The way the implementation works basically just relies on unwrapThenable and otherwise works like a plain Iterator. There is one quirk with these that are different than just promises. We ask for a new iterator each time we rerender. This means that upon retry we kick off another iteration which itself might kick off new requests that block iterating further. To solve this and make it actually efficient enough to use on the client we'd need to stash something like a buffer of the previous iteration and maybe iterator on the iterable so that we can continue where we left off or synchronously iterate if we've seen it before. Similar to our `.value` convention on Promises. In Fizz, I had to do a special case because when we render an iterator child we don't actually rerender the parent again like we do in Fiber. However, it's more efficient to just continue on where we left off by reusing the entries from the thenable state from before in that case. --- .../src/__tests__/ReactFlight-test.js | 61 ++------- .../src/__tests__/ReactDOMFizzServer-test.js | 68 +++++++++- .../react-reconciler/src/ReactChildFiber.js | 118 ++++++++++++++++- .../react-reconciler/src/ReactFiberHooks.js | 5 +- .../src/__tests__/ReactUse-test.js | 99 +++++++++++++++ packages/react-server/src/ReactFizzHooks.js | 22 +++- packages/react-server/src/ReactFizzServer.js | 119 +++++++++++++++++- .../react-server/src/ReactFizzThenable.js | 13 ++ packages/shared/ReactFeatureFlags.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + ...actFeatureFlags.test-renderer.native-fb.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 15 files changed, 447 insertions(+), 65 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 74b17db938..7071ef5081 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 64964031d5..dc514f2939 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 b11e8b59ad..e2dfe949a7 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 bad664adbc..59e7ae780f 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 467c68ff52..dede68854c 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 5caff3ca14..05cc1a2e1b 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 00ed4d8f06..b6925b1c7b 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 1991b9de1c..1494b4188e 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 545afd1cdc..25b93907cf 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 a4d0434f50..52c6f79d96 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 723a6a69c3..03c733dbc9 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 2271d9b2a2..565f257914 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 e6b299035a..59f7ecef41 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 44972804eb..6ba0619624 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 cf634cb168..4e252d59d6 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;