Skip to content

Commit b580fa3

Browse files
committed
Turn (Async)Generator into an (Async)Iterable if it's an (Async) Generator ServerComponent
1 parent 22ccf27 commit b580fa3

File tree

2 files changed

+133
-35
lines changed

2 files changed

+133
-35
lines changed

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

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,33 @@ describe('ReactFlight', () => {
295295
expect(Array.from(result)).toEqual([]);
296296
});
297297

298+
it('can render a Generator Server Component as a fragment', async () => {
299+
function ItemListClient(props) {
300+
return <span>{props.children}</span>;
301+
}
302+
const ItemList = clientReference(ItemListClient);
303+
304+
function* Items() {
305+
yield 'A';
306+
yield 'B';
307+
yield 'C';
308+
}
309+
310+
const model = (
311+
<ItemList>
312+
<Items />
313+
</ItemList>
314+
);
315+
316+
const transport = ReactNoopFlightServer.render(model);
317+
318+
await act(async () => {
319+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
320+
});
321+
322+
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
323+
});
324+
298325
it('can render undefined', async () => {
299326
function Undefined() {
300327
return undefined;
@@ -2151,16 +2178,9 @@ describe('ReactFlight', () => {
21512178
}
21522179
const Stateful = clientReference(StatefulClient);
21532180

2154-
function ServerComponent({item, initial}) {
2155-
// While the ServerComponent itself could be an async generator, single-shot iterables
2156-
// are not supported as React children since React might need to re-map them based on
2157-
// state updates. So we create an AsyncIterable instead.
2158-
return {
2159-
async *[Symbol.asyncIterator]() {
2160-
yield <Stateful key="a" initial={'a' + initial} />;
2161-
yield <Stateful key="b" initial={'b' + initial} />;
2162-
},
2163-
};
2181+
async function* ServerComponent({item, initial}) {
2182+
yield <Stateful key="a" initial={'a' + initial} />;
2183+
yield <Stateful key="b" initial={'b' + initial} />;
21642184
}
21652185

21662186
function ListClient({children}) {
@@ -2172,6 +2192,11 @@ describe('ReactFlight', () => {
21722192
expect(fragment.type).toBe(React.Fragment);
21732193
const fragmentChildren = [];
21742194
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+
}
21752200
for (let entry; !(entry = React.use(iterator.next())).done; ) {
21762201
fragmentChildren.push(entry.value);
21772202
}
@@ -2316,23 +2341,21 @@ describe('ReactFlight', () => {
23162341
let resolve;
23172342
const iteratorPromise = new Promise(r => (resolve = r));
23182343

2319-
function ThirdPartyAsyncIterableComponent({item, initial}) {
2320-
// While the ServerComponent itself could be an async generator, single-shot iterables
2321-
// are not supported as React children since React might need to re-map them based on
2322-
// state updates. So we create an AsyncIterable instead.
2323-
return {
2324-
async *[Symbol.asyncIterator]() {
2325-
yield <span>Who</span>;
2326-
yield <span>dis?</span>;
2327-
resolve();
2328-
},
2329-
};
2344+
async function* ThirdPartyAsyncIterableComponent({item, initial}) {
2345+
yield <span>Who</span>;
2346+
yield <span>dis?</span>;
2347+
resolve();
23302348
}
23312349

23322350
function ListClient({children: fragment}) {
23332351
// TODO: Unwrap AsyncIterables natively in React. For now we do it in this wrapper.
23342352
const resolvedChildren = [];
23352353
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+
}
23362359
for (let entry; !(entry = React.use(iterator.next())).done; ) {
23372360
resolvedChildren.push(entry.value);
23382361
}

packages/react-server/src/ReactFlightServer.js

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -866,20 +866,95 @@ function renderFunctionComponent<Props>(
866866
} else {
867867
result = Component(props, secondArg);
868868
}
869-
if (
870-
typeof result === 'object' &&
871-
result !== null &&
872-
typeof result.then === 'function'
873-
) {
874-
// When the return value is in children position we can resolve it immediately,
875-
// to its value without a wrapper if it's synchronously available.
876-
const thenable: Thenable<any> = result;
877-
if (thenable.status === 'fulfilled') {
878-
return thenable.value;
879-
}
880-
// TODO: Once we accept Promises as children on the client, we can just return
881-
// the thenable here.
882-
result = createLazyWrapperAroundWakeable(result);
869+
if (typeof result === 'object' && result !== null) {
870+
if (typeof result.then === 'function') {
871+
// When the return value is in children position we can resolve it immediately,
872+
// to its value without a wrapper if it's synchronously available.
873+
const thenable: Thenable<any> = result;
874+
if (thenable.status === 'fulfilled') {
875+
return thenable.value;
876+
}
877+
// TODO: Once we accept Promises as children on the client, we can just return
878+
// the thenable here.
879+
result = createLazyWrapperAroundWakeable(result);
880+
}
881+
882+
// Normally we'd serialize an Iterator/AsyncIterator as a single-shot which is not compatible
883+
// to be rendered as a React Child. However, because we have the function to recreate
884+
// an iterable from rendering the element again, we can effectively treat it as multi-
885+
// shot. Therefore we treat this as an Iterable/AsyncIterable, whether it was one or not, by
886+
// adding a wrapper so that this component effectively renders down to an AsyncIterable.
887+
const iteratorFn = getIteratorFn(result);
888+
if (iteratorFn) {
889+
const iterableChild = result;
890+
result = {
891+
[Symbol.iterator]: function () {
892+
const iterator = iteratorFn.call(iterableChild);
893+
if (__DEV__) {
894+
// If this was an Iterator but not a GeneratorFunction we warn because
895+
// it might have been a mistake. Technically you can make this mistake with
896+
// GeneratorFunctions and even single-shot Iterables too but it's extra
897+
// tempting to try to return the value from a generator.
898+
if (iterator === iterableChild) {
899+
const isGeneratorComponent =
900+
// $FlowIgnore[method-unbinding]
901+
Object.prototype.toString.call(Component) ===
902+
'[object GeneratorFunction]' &&
903+
// $FlowIgnore[method-unbinding]
904+
Object.prototype.toString.call(iterableChild) ===
905+
'[object Generator]';
906+
if (!isGeneratorComponent) {
907+
console.error(
908+
'Returning an Iterator from a Server Component is not supported ' +
909+
'since it cannot be looped over more than once. ',
910+
);
911+
}
912+
}
913+
}
914+
return (iterator: any);
915+
},
916+
};
917+
if (__DEV__) {
918+
(result: any)._debugInfo = iterableChild._debugInfo;
919+
}
920+
} else if (
921+
enableFlightReadableStream &&
922+
typeof (result: any)[ASYNC_ITERATOR] === 'function' &&
923+
(typeof ReadableStream !== 'function' ||
924+
!(result instanceof ReadableStream))
925+
) {
926+
const iterableChild = result;
927+
result = {
928+
[ASYNC_ITERATOR]: function () {
929+
const iterator = (iterableChild: any)[ASYNC_ITERATOR]();
930+
if (__DEV__) {
931+
// If this was an AsyncIterator but not an AsyncGeneratorFunction we warn because
932+
// it might have been a mistake. Technically you can make this mistake with
933+
// AsyncGeneratorFunctions and even single-shot AsyncIterables too but it's extra
934+
// tempting to try to return the value from a generator.
935+
if (iterator === iterableChild) {
936+
const isGeneratorComponent =
937+
// $FlowIgnore[method-unbinding]
938+
Object.prototype.toString.call(Component) ===
939+
'[object AsyncGeneratorFunction]' &&
940+
// $FlowIgnore[method-unbinding]
941+
Object.prototype.toString.call(iterableChild) ===
942+
'[object AsyncGenerator]';
943+
if (!isGeneratorComponent) {
944+
console.error(
945+
'Returning an AsyncIterator from a Server Component is not supported ' +
946+
'since it cannot be looped over more than once. ',
947+
);
948+
}
949+
}
950+
}
951+
return iterator;
952+
},
953+
};
954+
if (__DEV__) {
955+
(result: any)._debugInfo = iterableChild._debugInfo;
956+
}
957+
}
883958
}
884959
// Track this element's key on the Server Component on the keyPath context..
885960
const prevKeyPath = task.keyPath;

0 commit comments

Comments
 (0)