@samitouri / QOS-React / commits / 9f2eebd807

[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 `<SuspenseList>` 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.

Sebastian Markbåge committed Apr 22, 2024 at 13:25 UTC 9f2eebd807bf53b7d9901cf0b768762948224cae
15 files changed +447 -65
packages/react-client/src/__tests__/ReactFlight-test.js
+8 -53
@@ -2170,7 +2170,7 @@ describe('ReactFlight', () => {
2170 );
2171 });
2172
2173 - // @gate enableFlightReadableStream
2173 + // @gate enableFlightReadableStream && enableAsyncIterableChildren
2174 it('shares state when moving keyed Server Components that render async iterables', async () => {
2175 function StatefulClient({name, initial}) {
2176 const [state] = React.useState(initial);
@@ -2183,39 +2183,11 @@ describe('ReactFlight', () => {
2183 yield <Stateful key="b" initial={'b' + initial} />;
2184 }
2185
2186 - function ListClient({children}) {
2187 - // TODO: Unwrap AsyncIterables natively in React. For now we do it in this wrapper.
2188 - const resolvedChildren = [];
2189 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
2190 - for (const fragment of children) {
2191 - // We should've wrapped each child in a keyed Fragment.
2192 - expect(fragment.type).toBe(React.Fragment);
2193 - const fragmentChildren = [];
2194 - const iterator = fragment.props.children[Symbol.asyncIterator]();
2195 - if (iterator === fragment.props.children) {
2196 - console.error(
2197 - 'AyncIterators are not valid children of React. It must be a multi-shot AsyncIterable.',
2198 - );
2199 - }
2200 - for (let entry; !(entry = React.use(iterator.next())).done; ) {
2201 - fragmentChildren.push(entry.value);
2202 - }
2203 - resolvedChildren.push(
2204 - <React.Fragment key={fragment.key}>
2205 - {fragmentChildren}
2206 - </React.Fragment>,
2207 - );
2208 - }
2209 - return <div>{resolvedChildren}</div>;
2210 - }
2211 -
2212 - const List = clientReference(ListClient);
2213 -
2186 const transport = ReactNoopFlightServer.render(
2215 - <List>
2187 + <div>
2188 <ServerComponent key="A" initial={1} />
2189 <ServerComponent key="B" initial={2} />
2218 - </List>,
2190 + </div>,
2191 );
2192
2193 await act(async () => {
@@ -2234,10 +2206,10 @@ describe('ReactFlight', () => {
2206 // We swap the Server Components and the state of each child inside each fragment should move.
2207 // Really the Fragment itself moves.
2208 const transport2 = ReactNoopFlightServer.render(
2237 - <List>
2209 + <div>
2210 <ServerComponent key="B" initial={4} />
2211 <ServerComponent key="A" initial={3} />
2240 - </List>,
2212 + </div>,
2213 );
2214
2215 await act(async () => {
@@ -2336,7 +2308,7 @@ describe('ReactFlight', () => {
2308 );
2309 });
2310
2339 - // @gate enableFlightReadableStream
2311 + // @gate enableFlightReadableStream && enableAsyncIterableChildren
2312 it('preserves debug info for server-to-server pass through of async iterables', async () => {
2313 let resolve;
2314 const iteratorPromise = new Promise(r => (resolve = r));
@@ -2347,23 +2319,6 @@ describe('ReactFlight', () => {
2319 resolve();
2320 }
2321
2350 - function ListClient({children: fragment}) {
2351 - // TODO: Unwrap AsyncIterables natively in React. For now we do it in this wrapper.
2352 - const resolvedChildren = [];
2353 - const iterator = fragment.props.children[Symbol.asyncIterator]();
2354 - if (iterator === fragment.props.children) {
2355 - console.error(
2356 - 'AyncIterators are not valid children of React. It must be a multi-shot AsyncIterable.',
2357 - );
2358 - }
2359 - for (let entry; !(entry = React.use(iterator.next())).done; ) {
2360 - resolvedChildren.push(entry.value);
2361 - }
2362 - return <div>{resolvedChildren}</div>;
2363 - }
2364 -
2365 - const List = clientReference(ListClient);
2366 -
2322 function Keyed({children}) {
2323 // Keying this should generate a fragment.
2324 return children;
@@ -2375,9 +2330,9 @@ describe('ReactFlight', () => {
2330 ReactNoopFlightClient.read(transport),
2331 ).root;
2332 return (
2378 - <List>
2333 + <div>
2334 <Keyed key="keyed">{children}</Keyed>
2380 - </List>
2335 + </div>
2336 );
2337 }
2338
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+66 -2
@@ -3346,7 +3346,7 @@ describe('ReactDOMFizzServer', () => {
3346 ]);
3347 });
3348
3349 - it('Supports iterable', async () => {
3349 + it('supports iterable', async () => {
3350 const Immutable = require('immutable');
3351
3352 const mappedJSX = Immutable.fromJS([
@@ -3366,7 +3366,71 @@ describe('ReactDOMFizzServer', () => {
3366 );
3367 });
3368
3369 - it('Supports bigint', async () => {
3369 + // @gate enableAsyncIterableChildren
3370 + it('supports async generator component', async () => {
3371 + async function* App() {
3372 + yield <span key="1">{await Promise.resolve('Hi')}</span>;
3373 + yield ' ';
3374 + yield <span key="2">{await Promise.resolve('World')}</span>;
3375 + }
3376 +
3377 + await act(async () => {
3378 + const {pipe} = renderToPipeableStream(
3379 + <div>
3380 + <App />
3381 + </div>,
3382 + );
3383 + pipe(writable);
3384 + });
3385 +
3386 + // Each act retries once which causes a new ping which schedules
3387 + // new work but only after the act has finished rendering.
3388 + await act(() => {});
3389 + await act(() => {});
3390 + await act(() => {});
3391 + await act(() => {});
3392 +
3393 + expect(getVisibleChildren(container)).toEqual(
3394 + <div>
3395 + <span>Hi</span> <span>World</span>
3396 + </div>,
3397 + );
3398 + });
3399 +
3400 + // @gate enableAsyncIterableChildren
3401 + it('supports async iterable children', async () => {
3402 + const iterable = {
3403 + async *[Symbol.asyncIterator]() {
3404 + yield <span key="1">{await Promise.resolve('Hi')}</span>;
3405 + yield ' ';
3406 + yield <span key="2">{await Promise.resolve('World')}</span>;
3407 + },
3408 + };
3409 +
3410 + function App({children}) {
3411 + return <div>{children}</div>;
3412 + }
3413 +
3414 + await act(() => {
3415 + const {pipe} = renderToPipeableStream(<App>{iterable}</App>);
3416 + pipe(writable);
3417 + });
3418 +
3419 + // Each act retries once which causes a new ping which schedules
3420 + // new work but only after the act has finished rendering.
3421 + await act(() => {});
3422 + await act(() => {});
3423 + await act(() => {});
3424 + await act(() => {});
3425 +
3426 + expect(getVisibleChildren(container)).toEqual(
3427 + <div>
3428 + <span>Hi</span> <span>World</span>
3429 + </div>,
3430 + );
3431 + });
3432 +
3433 + it('supports bigint', async () => {
3434 await act(async () => {
3435 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3436 <div>{10n}</div>,
packages/react-reconciler/src/ReactChildFiber.js
+112 -6
@@ -27,6 +27,7 @@ import {
27 } from './ReactFiberFlags';
28 import {
29 getIteratorFn,
30 + ASYNC_ITERATOR,
31 REACT_ELEMENT_TYPE,
32 REACT_FRAGMENT_TYPE,
33 REACT_PORTAL_TYPE,
@@ -42,7 +43,10 @@ import {
43 FunctionComponent,
44 } from './ReactWorkTags';
45 import isArray from 'shared/isArray';
45 -import {enableRefAsProp} from 'shared/ReactFeatureFlags';
46 +import {
47 + enableRefAsProp,
48 + enableAsyncIterableChildren,
49 +} from 'shared/ReactFeatureFlags';
50
51 import {
52 createWorkInProgress,
@@ -587,7 +591,12 @@ function createChildReconciler(
591 }
592 }
593
590 - if (isArray(newChild) || getIteratorFn(newChild)) {
594 + if (
595 + isArray(newChild) ||
596 + getIteratorFn(newChild) ||
597 + (enableAsyncIterableChildren &&
598 + typeof newChild[ASYNC_ITERATOR] === 'function')
599 + ) {
600 const created = createFiberFromFragment(
601 newChild,
602 returnFiber.mode,
@@ -711,7 +720,12 @@ function createChildReconciler(
720 }
721 }
722
714 - if (isArray(newChild) || getIteratorFn(newChild)) {
723 + if (
724 + isArray(newChild) ||
725 + getIteratorFn(newChild) ||
726 + (enableAsyncIterableChildren &&
727 + typeof newChild[ASYNC_ITERATOR] === 'function')
728 + ) {
729 if (key !== null) {
730 return null;
731 }
@@ -833,7 +847,12 @@ function createChildReconciler(
847 );
848 }
849
836 - if (isArray(newChild) || getIteratorFn(newChild)) {
850 + if (
851 + isArray(newChild) ||
852 + getIteratorFn(newChild) ||
853 + (enableAsyncIterableChildren &&
854 + typeof newChild[ASYNC_ITERATOR] === 'function')
855 + ) {
856 const matchedFiber = existingChildren.get(newIdx) || null;
857 return updateFragment(
858 returnFiber,
@@ -1112,7 +1131,7 @@ function createChildReconciler(
1131 return resultingFirstChild;
1132 }
1133
1115 - function reconcileChildrenIterator(
1134 + function reconcileChildrenIteratable(
1135 returnFiber: Fiber,
1136 currentFirstChild: Fiber | null,
1137 newChildrenIterable: Iterable<mixed>,
@@ -1171,6 +1190,80 @@ function createChildReconciler(
1190 }
1191 }
1192
1193 + return reconcileChildrenIterator(
1194 + returnFiber,
1195 + currentFirstChild,
1196 + newChildren,
1197 + lanes,
1198 + debugInfo,
1199 + );
1200 + }
1201 +
1202 + function reconcileChildrenAsyncIteratable(
1203 + returnFiber: Fiber,
1204 + currentFirstChild: Fiber | null,
1205 + newChildrenIterable: AsyncIterable<mixed>,
1206 + lanes: Lanes,
1207 + debugInfo: ReactDebugInfo | null,
1208 + ): Fiber | null {
1209 + const newChildren = newChildrenIterable[ASYNC_ITERATOR]();
1210 +
1211 + if (__DEV__) {
1212 + if (newChildren === newChildrenIterable) {
1213 + // We don't support rendering AsyncGenerators as props because it's a mutation.
1214 + // We do support generators if they were created by a AsyncGeneratorFunction component
1215 + // as its direct child since we can recreate those by rerendering the component
1216 + // as needed.
1217 + const isGeneratorComponent =
1218 + returnFiber.tag === FunctionComponent &&
1219 + // $FlowFixMe[method-unbinding]
1220 + Object.prototype.toString.call(returnFiber.type) ===
1221 + '[object AsyncGeneratorFunction]' &&
1222 + // $FlowFixMe[method-unbinding]
1223 + Object.prototype.toString.call(newChildren) ===
1224 + '[object AsyncGenerator]';
1225 + if (!isGeneratorComponent) {
1226 + if (!didWarnAboutGenerators) {
1227 + console.error(
1228 + 'Using AsyncIterators as children is unsupported and will likely yield ' +
1229 + 'unexpected results because enumerating a generator mutates it. ' +
1230 + 'You can use an AsyncIterable that can iterate multiple times over ' +
1231 + 'the same items.',
1232 + );
1233 + }
1234 + didWarnAboutGenerators = true;
1235 + }
1236 + }
1237 + }
1238 +
1239 + if (newChildren == null) {
1240 + throw new Error('An iterable object provided no iterator.');
1241 + }
1242 +
1243 + // To save bytes, we reuse the logic by creating a synchronous Iterable and
1244 + // reusing that code path.
1245 + const iterator: Iterator<mixed> = ({
1246 + next(): IteratorResult<mixed, void> {
1247 + return unwrapThenable(newChildren.next());
1248 + },
1249 + }: any);
1250 +
1251 + return reconcileChildrenIterator(
1252 + returnFiber,
1253 + currentFirstChild,
1254 + iterator,
1255 + lanes,
1256 + debugInfo,
1257 + );
1258 + }
1259 +
1260 + function reconcileChildrenIterator(
1261 + returnFiber: Fiber,
1262 + currentFirstChild: Fiber | null,
1263 + newChildren: ?Iterator<mixed>,
1264 + lanes: Lanes,
1265 + debugInfo: ReactDebugInfo | null,
1266 + ): Fiber | null {
1267 if (newChildren == null) {
1268 throw new Error('An iterable object provided no iterator.');
1269 }
@@ -1563,7 +1656,20 @@ function createChildReconciler(
1656 }
1657
1658 if (getIteratorFn(newChild)) {
1566 - return reconcileChildrenIterator(
1659 + return reconcileChildrenIteratable(
1660 + returnFiber,
1661 + currentFirstChild,
1662 + newChild,
1663 + lanes,
1664 + mergeDebugInfo(debugInfo, newChild._debugInfo),
1665 + );
1666 + }
1667 +
1668 + if (
1669 + enableAsyncIterableChildren &&
1670 + typeof newChild[ASYNC_ITERATOR] === 'function'
1671 + ) {
1672 + return reconcileChildrenAsyncIteratable(
1673 returnFiber,
1674 currentFirstChild,
1675 newChild,
packages/react-reconciler/src/ReactFiberHooks.js
+4 -1
@@ -414,7 +414,10 @@ function warnIfAsyncClientComponent(Component: Function) {
414 // bulletproof but together they cover the most common cases.
415 const isAsyncFunction =
416 // $FlowIgnore[method-unbinding]
417 - Object.prototype.toString.call(Component) === '[object AsyncFunction]';
417 + Object.prototype.toString.call(Component) === '[object AsyncFunction]' ||
418 + // $FlowIgnore[method-unbinding]
419 + Object.prototype.toString.call(Component) ===
420 + '[object AsyncGeneratorFunction]';
421 if (isAsyncFunction) {
422 // Encountered an async Client Component. This is not yet supported.
423 const componentName = getComponentNameFromFiber(currentlyRenderingFiber);
packages/react-reconciler/src/__tests__/ReactUse-test.js
+99
@@ -1,3 +1,12 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 'use strict';
11
12 let React;
@@ -1816,4 +1825,94 @@ describe('ReactUse', () => {
1825 'supported, except via a Suspense-compatible library or framework.',
1826 ]);
1827 });
1828 +
1829 + // @gate enableAsyncIterableChildren
1830 + test('async generator component', async () => {
1831 + let hi, world;
1832 + async function* App() {
1833 + // Only cached promises can be awaited in async generators because
1834 + // when we rerender, it'll issue another request which blocks the next.
1835 + await (hi || (hi = getAsyncText('Hi')));
1836 + yield <Text key="1" text="Hi" />;
1837 + yield ' ';
1838 + await (world || (world = getAsyncText('World')));
1839 + yield <Text key="2" text="World" />;
1840 + }
1841 +
1842 + const root = ReactNoop.createRoot();
1843 + await expect(async () => {
1844 + await act(() => {
1845 + startTransition(() => {
1846 + root.render(<App />);
1847 + });
1848 + });
1849 + }).toErrorDev([
1850 + 'async/await is not yet supported in Client Components, only ' +
1851 + 'Server Components. This error is often caused by accidentally ' +
1852 + "adding `'use client'` to a module that was originally written " +
1853 + 'for the server.',
1854 + ]);
1855 + assertLog(['Async text requested [Hi]']);
1856 +
1857 + await expect(async () => {
1858 + await act(() => resolveTextRequests('Hi'));
1859 + }).toErrorDev(
1860 + // We get this warning because the generator's promise themselves are not cached.
1861 + 'A component was suspended by an uncached promise. Creating ' +
1862 + 'promises inside a Client Component or hook is not yet ' +
1863 + 'supported, except via a Suspense-compatible library or framework.',
1864 + );
1865 +
1866 + assertLog(['Async text requested [World]']);
1867 +
1868 + await act(() => resolveTextRequests('World'));
1869 +
1870 + assertLog(['Hi', 'World']);
1871 + expect(root).toMatchRenderedOutput('Hi World');
1872 + });
1873 +
1874 + // @gate enableAsyncIterableChildren
1875 + test('async iterable children', async () => {
1876 + let hi, world;
1877 + const iterable = {
1878 + async *[Symbol.asyncIterator]() {
1879 + // Only cached promises can be awaited in async iterables because
1880 + // when we retry, it'll ask for another iterator which issues another
1881 + // request which blocks the next.
1882 + await (hi || (hi = getAsyncText('Hi')));
1883 + yield <Text key="1" text="Hi" />;
1884 + yield ' ';
1885 + await (world || (world = getAsyncText('World')));
1886 + yield <Text key="2" text="World" />;
1887 + },
1888 + };
1889 +
1890 + function App({children}) {
1891 + return <div>{children}</div>;
1892 + }
1893 +
1894 + const root = ReactNoop.createRoot();
1895 + await act(() => {
1896 + startTransition(() => {
1897 + root.render(<App>{iterable}</App>);
1898 + });
1899 + });
1900 + assertLog(['Async text requested [Hi]']);
1901 +
1902 + await expect(async () => {
1903 + await act(() => resolveTextRequests('Hi'));
1904 + }).toErrorDev(
1905 + // We get this warning because the generator's promise themselves are not cached.
1906 + 'A component was suspended by an uncached promise. Creating ' +
1907 + 'promises inside a Client Component or hook is not yet ' +
1908 + 'supported, except via a Suspense-compatible library or framework.',
1909 + );
1910 +
1911 + assertLog(['Async text requested [World]']);
1912 +
1913 + await act(() => resolveTextRequests('World'));
1914 +
1915 + assertLog(['Hi', 'World']);
1916 + expect(root).toMatchRenderedOutput(<div>Hi World</div>);
1917 + });
1918 });
packages/react-server/src/ReactFizzHooks.js
+21 -1
@@ -25,7 +25,11 @@ import type {TransitionStatus} from './ReactFizzConfig';
25
26 import {readContext as readContextImpl} from './ReactFizzNewContext';
27 import {getTreeId} from './ReactFizzTreeContext';
28 -import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
28 +import {
29 + createThenableState,
30 + trackUsedThenable,
31 + readPreviousThenable,
32 +} from './ReactFizzThenable';
33
34 import {makeId, NotPendingTransition} from './ReactFizzConfig';
35 import {createFastHash} from './ReactServerStreamConfig';
@@ -229,6 +233,13 @@ export function prepareToUseHooks(
233 thenableState = prevThenableState;
234 }
235
236 +export function prepareToUseThenableState(
237 + prevThenableState: ThenableState | null,
238 +): void {
239 + thenableIndexCounter = 0;
240 + thenableState = prevThenableState;
241 +}
242 +
243 export function finishHooks(
244 Component: any,
245 props: any,
@@ -765,6 +776,15 @@ export function unwrapThenable<T>(thenable: Thenable<T>): T {
776 return trackUsedThenable(thenableState, thenable, index);
777 }
778
779 +export function readPreviousThenableFromState<T>(): T | void {
780 + const index = thenableIndexCounter;
781 + thenableIndexCounter += 1;
782 + if (thenableState === null) {
783 + return undefined;
784 + }
785 + return readPreviousThenable(thenableState, index);
786 +}
787 +
788 function unsupportedRefresh() {
789 throw new Error('Cache cannot be refreshed during server rendering.');
790 }
packages/react-server/src/ReactFizzServer.js
+117 -2
@@ -98,6 +98,7 @@ import {
98 } from './ReactFizzNewContext';
99 import {
100 prepareToUseHooks,
101 + prepareToUseThenableState,
102 finishHooks,
103 checkDidRenderIdHook,
104 resetHooksState,
@@ -106,6 +107,7 @@ import {
107 setCurrentResumableState,
108 getThenableStateAfterSuspending,
109 unwrapThenable,
110 + readPreviousThenableFromState,
111 getActionStateCount,
112 getActionStateMatchingIndex,
113 } from './ReactFizzHooks';
@@ -115,6 +117,7 @@ import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
117
118 import {
119 getIteratorFn,
120 + ASYNC_ITERATOR,
121 REACT_ELEMENT_TYPE,
122 REACT_PORTAL_TYPE,
123 REACT_LAZY_TYPE,
@@ -144,6 +147,7 @@ import {
147 enableRenderableContext,
148 enableRefAsProp,
149 disableDefaultPropsExceptForClasses,
150 + enableAsyncIterableChildren,
151 } from 'shared/ReactFeatureFlags';
152
153 import assign from 'shared/assign';
@@ -2165,6 +2169,7 @@ function validateIterable(
2169 // as its direct child since we can recreate those by rerendering the component
2170 // as needed.
2171 const isGeneratorComponent =
2172 + childIndex === -1 && // Only the root child is valid
2173 task.componentStack !== null &&
2174 task.componentStack.tag === 1 && // FunctionComponent
2175 // $FlowFixMe[method-unbinding]
@@ -2197,6 +2202,43 @@ function validateIterable(
2202 }
2203 }
2204
2205 +function validateAsyncIterable(
2206 + task: Task,
2207 + iterable: AsyncIterable<any>,
2208 + childIndex: number,
2209 + iterator: AsyncIterator<any>,
2210 +): void {
2211 + if (__DEV__) {
2212 + if (iterator === iterable) {
2213 + // We don't support rendering Generators as props because it's a mutation.
2214 + // See https://github.com/facebook/react/issues/12995
2215 + // We do support generators if they were created by a GeneratorFunction component
2216 + // as its direct child since we can recreate those by rerendering the component
2217 + // as needed.
2218 + const isGeneratorComponent =
2219 + childIndex === -1 && // Only the root child is valid
2220 + task.componentStack !== null &&
2221 + task.componentStack.tag === 1 && // FunctionComponent
2222 + // $FlowFixMe[method-unbinding]
2223 + Object.prototype.toString.call(task.componentStack.type) ===
2224 + '[object AsyncGeneratorFunction]' &&
2225 + // $FlowFixMe[method-unbinding]
2226 + Object.prototype.toString.call(iterator) === '[object AsyncGenerator]';
2227 + if (!isGeneratorComponent) {
2228 + if (!didWarnAboutGenerators) {
2229 + console.error(
2230 + 'Using AsyncIterators as children is unsupported and will likely yield ' +
2231 + 'unexpected results because enumerating a generator mutates it. ' +
2232 + 'You can use an AsyncIterable that can iterate multiple times over ' +
2233 + 'the same items.',
2234 + );
2235 + }
2236 + didWarnAboutGenerators = true;
2237 + }
2238 + }
2239 + }
2240 +}
2241 +
2242 function warnOnFunctionType(invalidChild: Function) {
2243 if (__DEV__) {
2244 const name = invalidChild.displayName || invalidChild.name || 'Component';
@@ -2327,7 +2369,6 @@ function renderNodeDestructive(
2369 // TODO: This is not great but I think it's inherent to the id
2370 // generation algorithm.
2371 let step = iterator.next();
2330 - // If there are not entries, we need to push an empty so we start by checking that.
2372 if (!step.done) {
2373 const children = [];
2374 do {
@@ -2335,12 +2376,76 @@ function renderNodeDestructive(
2376 step = iterator.next();
2377 } while (!step.done);
2378 renderChildrenArray(request, task, children, childIndex);
2338 - return;
2379 }
2380 return;
2381 }
2382 }
2383
2384 + if (
2385 + enableAsyncIterableChildren &&
2386 + typeof (node: any)[ASYNC_ITERATOR] === 'function'
2387 + ) {
2388 + const iterator: AsyncIterator<ReactNodeList> = (node: any)[
2389 + ASYNC_ITERATOR
2390 + ]();
2391 + if (iterator) {
2392 + if (__DEV__) {
2393 + validateAsyncIterable(task, (node: any), childIndex, iterator);
2394 + }
2395 + // TODO: Update the task.node to be the iterator to avoid asking
2396 + // for new iterators, but we currently warn for rendering these
2397 + // so needs some refactoring to deal with the warning.
2398 +
2399 + // We need to push a component stack because if this suspends, we'll pop a stack.
2400 + const previousComponentStack = task.componentStack;
2401 + task.componentStack = createBuiltInComponentStack(
2402 + task,
2403 + 'AsyncIterable',
2404 + );
2405 +
2406 + // Restore the thenable state before resuming.
2407 + const prevThenableState = task.thenableState;
2408 + task.thenableState = null;
2409 + prepareToUseThenableState(prevThenableState);
2410 +
2411 + // We need to know how many total children are in this set, so that we
2412 + // can allocate enough id slots to acommodate them. So we must exhaust
2413 + // the iterator before we start recursively rendering the children.
2414 + // TODO: This is not great but I think it's inherent to the id
2415 + // generation algorithm.
2416 + const children = [];
2417 +
2418 + let done = false;
2419 +
2420 + if (iterator === node) {
2421 + // If it's an iterator we need to continue reading where we left
2422 + // off. We can do that by reading the first few rows from the previous
2423 + // thenable state.
2424 + // $FlowFixMe
2425 + let step = readPreviousThenableFromState();
2426 + while (step !== undefined) {
2427 + if (step.done) {
2428 + done = true;
2429 + break;
2430 + }
2431 + children.push(step.value);
2432 + step = readPreviousThenableFromState();
2433 + }
2434 + }
2435 +
2436 + if (!done) {
2437 + let step = unwrapThenable(iterator.next());
2438 + while (!step.done) {
2439 + children.push(step.value);
2440 + step = unwrapThenable(iterator.next());
2441 + }
2442 + }
2443 + task.componentStack = previousComponentStack;
2444 + renderChildrenArray(request, task, children, childIndex);
2445 + return;
2446 + }
2447 + }
2448 +
2449 // Usables are a valid React node type. When React encounters a Usable in
2450 // a child position, it unwraps it using the same algorithm as `use`. For
2451 // example, for promises, React will throw an exception to unwind the
@@ -3554,6 +3659,11 @@ function retryRenderTask(
3659 const ping = task.ping;
3660 x.then(ping, ping);
3661 task.thenableState = getThenableStateAfterSuspending();
3662 + // We pop one task off the stack because the node that suspended will be tried again,
3663 + // which will add it back onto the stack.
3664 + if (task.componentStack !== null) {
3665 + task.componentStack = task.componentStack.parent;
3666 + }
3667 return;
3668 } else if (
3669 enablePostpone &&
@@ -3639,6 +3749,11 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
3749 const ping = task.ping;
3750 x.then(ping, ping);
3751 task.thenableState = getThenableStateAfterSuspending();
3752 + // We pop one task off the stack because the node that suspended will be tried again,
3753 + // which will add it back onto the stack.
3754 + if (task.componentStack !== null) {
3755 + task.componentStack = task.componentStack.parent;
3756 + }
3757 return;
3758 }
3759 }
packages/react-server/src/ReactFizzThenable.js
+13
@@ -131,6 +131,19 @@ export function trackUsedThenable<T>(
131 }
132 }
133
134 +export function readPreviousThenable<T>(
135 + thenableState: ThenableState,
136 + index: number,
137 +): void | T {
138 + const previous = thenableState[index];
139 + if (previous === undefined) {
140 + return undefined;
141 + } else {
142 + // We assume this has been resolved already.
143 + return (previous: any).value;
144 + }
145 +}
146 +
147 // This is used to track the actual thenable that suspended so it can be
148 // passed to the rest of the Suspense implementation — which, for historical
149 // reasons, expects to receive a thenable.
packages/shared/ReactFeatureFlags.js
+1
@@ -82,6 +82,7 @@ export const enableFetchInstrumentation = true;
82
83 export const enableBinaryFlight = __EXPERIMENTAL__;
84 export const enableFlightReadableStream = __EXPERIMENTAL__;
85 +export const enableAsyncIterableChildren = __EXPERIMENTAL__;
86
87 export const enableTaint = __EXPERIMENTAL__;
88
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -46,6 +46,7 @@ export const enableLegacyCache = false;
46 export const enableFetchInstrumentation = false;
47 export const enableBinaryFlight = true;
48 export const enableFlightReadableStream = true;
49 +export const enableAsyncIterableChildren = false;
50 export const enableTaint = true;
51 export const enablePostpone = false;
52 export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -103,6 +103,7 @@ export const enableTransitionTracing = false;
103 export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
104 export const passChildrenWhenCloningPersistedNodes = false;
105 export const enableEarlyReturnForPropDiffing = false;
106 +export const enableAsyncIterableChildren = false;
107
108 export const renameElementSymbol = true;
109
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -23,6 +23,7 @@ export const enableLegacyCache = __EXPERIMENTAL__;
23 export const enableFetchInstrumentation = true;
24 export const enableBinaryFlight = true;
25 export const enableFlightReadableStream = true;
26 +export const enableAsyncIterableChildren = false;
27 export const enableTaint = true;
28 export const enablePostpone = false;
29 export const disableCommentsAsDOMContainers = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -23,6 +23,7 @@ export const enableLegacyCache = false;
23 export const enableFetchInstrumentation = false;
24 export const enableBinaryFlight = true;
25 export const enableFlightReadableStream = true;
26 +export const enableAsyncIterableChildren = false;
27 export const enableTaint = true;
28 export const enablePostpone = false;
29 export const disableCommentsAsDOMContainers = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -23,6 +23,7 @@ export const enableLegacyCache = true;
23 export const enableFetchInstrumentation = false;
24 export const enableBinaryFlight = true;
25 export const enableFlightReadableStream = true;
26 +export const enableAsyncIterableChildren = false;
27 export const enableTaint = true;
28 export const enablePostpone = false;
29 export const disableCommentsAsDOMContainers = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -73,6 +73,7 @@ export const enableFetchInstrumentation = false;
73
74 export const enableBinaryFlight = false;
75 export const enableFlightReadableStream = false;
76 +export const enableAsyncIterableChildren = false;
77
78 export const enableTaint = false;
79