Skip to content

Commit 616e405

Browse files
committed
Turn AsyncGenerator into an AsyncIterable if it's an Async Generator ServerComponent
1 parent 7909d8e commit 616e405

File tree

2 files changed

+90
-34
lines changed

2 files changed

+90
-34
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,29 @@ describe('ReactFlight', () => {
277277
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
278278
});
279279

280+
it('can render a Generator Server Component as a fragment', async () => {
281+
function ItemListClient(props) {
282+
return <span>{props.children}</span>;
283+
}
284+
const ItemList = clientReference(ItemListClient);
285+
286+
function * Items() {
287+
yield 'A';
288+
yield 'B';
289+
yield 'C';
290+
}
291+
292+
const model = <ItemList><Items /></ItemList>;
293+
294+
const transport = ReactNoopFlightServer.render(model);
295+
296+
await act(async () => {
297+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
298+
});
299+
300+
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
301+
});
302+
280303
it('can render undefined', async () => {
281304
function Undefined() {
282305
return undefined;
@@ -2133,16 +2156,9 @@ describe('ReactFlight', () => {
21332156
}
21342157
const Stateful = clientReference(StatefulClient);
21352158

2136-
function ServerComponent({item, initial}) {
2137-
// While the ServerComponent itself could be an async generator, single-shot iterables
2138-
// are not supported as React children since React might need to re-map them based on
2139-
// state updates. So we create an AsyncIterable instead.
2140-
return {
2141-
async *[Symbol.asyncIterator]() {
2142-
yield <Stateful key="a" initial={'a' + initial} />;
2143-
yield <Stateful key="b" initial={'b' + initial} />;
2144-
},
2145-
};
2159+
async function* ServerComponent({item, initial}) {
2160+
yield <Stateful key="a" initial={'a' + initial} />;
2161+
yield <Stateful key="b" initial={'b' + initial} />;
21462162
}
21472163

21482164
function ListClient({children}) {
@@ -2154,6 +2170,11 @@ describe('ReactFlight', () => {
21542170
expect(fragment.type).toBe(React.Fragment);
21552171
const fragmentChildren = [];
21562172
const iterator = fragment.props.children[Symbol.asyncIterator]();
2173+
if (iterator === fragment.props.children) {
2174+
console.error(
2175+
'AyncIterators are not valid children of React. It must be a multi-shot AsyncIterable.',
2176+
);
2177+
}
21572178
for (let entry; !(entry = React.use(iterator.next())).done; ) {
21582179
fragmentChildren.push(entry.value);
21592180
}
@@ -2298,23 +2319,21 @@ describe('ReactFlight', () => {
22982319
let resolve;
22992320
const iteratorPromise = new Promise(r => (resolve = r));
23002321

2301-
function ThirdPartyAsyncIterableComponent({item, initial}) {
2302-
// While the ServerComponent itself could be an async generator, single-shot iterables
2303-
// are not supported as React children since React might need to re-map them based on
2304-
// state updates. So we create an AsyncIterable instead.
2305-
return {
2306-
async *[Symbol.asyncIterator]() {
2307-
yield <span>Who</span>;
2308-
yield <span>dis?</span>;
2309-
resolve();
2310-
},
2311-
};
2322+
async function* ThirdPartyAsyncIterableComponent({item, initial}) {
2323+
yield <span>Who</span>;
2324+
yield <span>dis?</span>;
2325+
resolve();
23122326
}
23132327

23142328
function ListClient({children: fragment}) {
23152329
// TODO: Unwrap AsyncIterables natively in React. For now we do it in this wrapper.
23162330
const resolvedChildren = [];
23172331
const iterator = fragment.props.children[Symbol.asyncIterator]();
2332+
if (iterator === fragment.props.children) {
2333+
console.error(
2334+
'AyncIterators are not valid children of React. It must be a multi-shot AsyncIterable.',
2335+
);
2336+
}
23182337
for (let entry; !(entry = React.use(iterator.next())).done; ) {
23192338
resolvedChildren.push(entry.value);
23202339
}

packages/react-server/src/ReactFlightServer.js

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -864,20 +864,55 @@ function renderFunctionComponent<Props>(
864864
} else {
865865
result = Component(props, secondArg);
866866
}
867-
if (
868-
typeof result === 'object' &&
869-
result !== null &&
870-
typeof result.then === 'function'
871-
) {
872-
// When the return value is in children position we can resolve it immediately,
873-
// to its value without a wrapper if it's synchronously available.
874-
const thenable: Thenable<any> = result;
875-
if (thenable.status === 'fulfilled') {
876-
return thenable.value;
867+
if (typeof result === 'object' && result !== null) {
868+
if (typeof result.then === 'function') {
869+
// When the return value is in children position we can resolve it immediately,
870+
// to its value without a wrapper if it's synchronously available.
871+
const thenable: Thenable<any> = result;
872+
if (thenable.status === 'fulfilled') {
873+
return thenable.value;
874+
}
875+
// TODO: Once we accept Promises as children on the client, we can just return
876+
// the thenable here.
877+
result = createLazyWrapperAroundWakeable(result);
878+
}
879+
if (
880+
enableFlightReadableStream &&
881+
typeof (result: any)[ASYNC_ITERATOR] === 'function' &&
882+
(typeof ReadableStream !== 'function' ||
883+
!(result instanceof ReadableStream))
884+
) {
885+
// Normally we'd serialize an AsyncIterator as a single-shot which is not compatible
886+
// to be rendered as a React Child. However, because we have the function to recreate
887+
// an iterable from rendering the element again, we can effectively treat it as multi-
888+
// shot. Therefore we treat this as an AsyncIterable, whether it was one or not, by
889+
// adding a wrapper so that this component effectively renders down to an AsyncIterable.
890+
const iterableChild = result;
891+
result = {
892+
[ASYNC_ITERATOR]: function () {
893+
const iterator = (iterableChild: any)[ASYNC_ITERATOR]();
894+
if (__DEV__) {
895+
// If this was an AsyncIterator but not an AsyncGeneratorFunction we warn because
896+
// it might have been a mistake. Technically you can make this mistake with
897+
// AsyncGeneratorFunctions and even single-shot AsyncIterables too but it's extra
898+
// tempting to try to return the value from a generator.
899+
if (
900+
iterator === iterableChild &&
901+
// $FlowIgnore[method-unbinding]
902+
Object.prototype.toString.call(iterableChild) !==
903+
'[object AsyncGenerator]'
904+
) {
905+
console.error(
906+
'Returning an AsyncIterator from a Server Component is not supported ' +
907+
'since it cannot be looped over more than once. ',
908+
);
909+
}
910+
}
911+
return iterator;
912+
},
913+
_debugInfo: iterableChild._debugInfo,
914+
};
877915
}
878-
// TODO: Once we accept Promises as children on the client, we can just return
879-
// the thenable here.
880-
result = createLazyWrapperAroundWakeable(result);
881916
}
882917
// Track this element's key on the Server Component on the keyPath context..
883918
const prevKeyPath = task.keyPath;
@@ -1911,6 +1946,8 @@ function renderModelDestructive(
19111946

19121947
const iteratorFn = getIteratorFn(value);
19131948
if (iteratorFn) {
1949+
// TODO: Distinguish Iterator and Iterable since client-side React should
1950+
// warn if you try to use an Iterator.
19141951
return renderFragment(request, task, Array.from((value: any)));
19151952
}
19161953

0 commit comments

Comments
 (0)