Skip to content

Commit 7778cc2

Browse files
committed
Turn (Async)Generator into an (Async)Iterable if it's an (Async) Generator ServerComponent
1 parent bf426f9 commit 7778cc2

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

0 commit comments

Comments
 (0)