Skip to content

Commit 3d2ab01

Browse files
authored
[Flight] Extract special cases for Server Component return value position (facebook#31713)
This is just moving some code into a helper. We have a bunch of special cases for the return value slot of a Server Component that's different from just rendering that inside an object. This was getting a little tricky to reason about inline with the rest of rendering.
1 parent 76d603a commit 3d2ab01

File tree

1 file changed

+139
-116
lines changed

1 file changed

+139
-116
lines changed

packages/react-server/src/ReactFlightServer.js

+139-116
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,143 @@ function callWithDebugContextInDEV<A, T>(
11051105

11061106
const voidHandler = () => {};
11071107

1108+
function processServerComponentReturnValue(
1109+
request: Request,
1110+
task: Task,
1111+
Component: any,
1112+
result: any,
1113+
): any {
1114+
// A Server Component's return value has a few special properties due to being
1115+
// in the return position of a Component. We convert them here.
1116+
if (
1117+
typeof result !== 'object' ||
1118+
result === null ||
1119+
isClientReference(result)
1120+
) {
1121+
return result;
1122+
}
1123+
1124+
if (typeof result.then === 'function') {
1125+
// When the return value is in children position we can resolve it immediately,
1126+
// to its value without a wrapper if it's synchronously available.
1127+
const thenable: Thenable<any> = result;
1128+
if (__DEV__) {
1129+
// If the thenable resolves to an element, then it was in a static position,
1130+
// the return value of a Server Component. That doesn't need further validation
1131+
// of keys. The Server Component itself would have had a key.
1132+
thenable.then(resolvedValue => {
1133+
if (
1134+
typeof resolvedValue === 'object' &&
1135+
resolvedValue !== null &&
1136+
resolvedValue.$$typeof === REACT_ELEMENT_TYPE
1137+
) {
1138+
resolvedValue._store.validated = 1;
1139+
}
1140+
}, voidHandler);
1141+
}
1142+
if (thenable.status === 'fulfilled') {
1143+
return thenable.value;
1144+
}
1145+
// TODO: Once we accept Promises as children on the client, we can just return
1146+
// the thenable here.
1147+
return createLazyWrapperAroundWakeable(result);
1148+
}
1149+
1150+
if (__DEV__) {
1151+
if ((result: any).$$typeof === REACT_ELEMENT_TYPE) {
1152+
// If the server component renders to an element, then it was in a static position.
1153+
// That doesn't need further validation of keys. The Server Component itself would
1154+
// have had a key.
1155+
(result: any)._store.validated = 1;
1156+
}
1157+
}
1158+
1159+
// Normally we'd serialize an Iterator/AsyncIterator as a single-shot which is not compatible
1160+
// to be rendered as a React Child. However, because we have the function to recreate
1161+
// an iterable from rendering the element again, we can effectively treat it as multi-
1162+
// shot. Therefore we treat this as an Iterable/AsyncIterable, whether it was one or not, by
1163+
// adding a wrapper so that this component effectively renders down to an AsyncIterable.
1164+
const iteratorFn = getIteratorFn(result);
1165+
if (iteratorFn) {
1166+
const iterableChild = result;
1167+
const multiShot = {
1168+
[Symbol.iterator]: function () {
1169+
const iterator = iteratorFn.call(iterableChild);
1170+
if (__DEV__) {
1171+
// If this was an Iterator but not a GeneratorFunction we warn because
1172+
// it might have been a mistake. Technically you can make this mistake with
1173+
// GeneratorFunctions and even single-shot Iterables too but it's extra
1174+
// tempting to try to return the value from a generator.
1175+
if (iterator === iterableChild) {
1176+
const isGeneratorComponent =
1177+
// $FlowIgnore[method-unbinding]
1178+
Object.prototype.toString.call(Component) ===
1179+
'[object GeneratorFunction]' &&
1180+
// $FlowIgnore[method-unbinding]
1181+
Object.prototype.toString.call(iterableChild) ===
1182+
'[object Generator]';
1183+
if (!isGeneratorComponent) {
1184+
callWithDebugContextInDEV(request, task, () => {
1185+
console.error(
1186+
'Returning an Iterator from a Server Component is not supported ' +
1187+
'since it cannot be looped over more than once. ',
1188+
);
1189+
});
1190+
}
1191+
}
1192+
}
1193+
return (iterator: any);
1194+
},
1195+
};
1196+
if (__DEV__) {
1197+
(multiShot: any)._debugInfo = iterableChild._debugInfo;
1198+
}
1199+
return multiShot;
1200+
}
1201+
if (
1202+
enableFlightReadableStream &&
1203+
typeof (result: any)[ASYNC_ITERATOR] === 'function' &&
1204+
(typeof ReadableStream !== 'function' ||
1205+
!(result instanceof ReadableStream))
1206+
) {
1207+
const iterableChild = result;
1208+
const multishot = {
1209+
[ASYNC_ITERATOR]: function () {
1210+
const iterator = (iterableChild: any)[ASYNC_ITERATOR]();
1211+
if (__DEV__) {
1212+
// If this was an AsyncIterator but not an AsyncGeneratorFunction we warn because
1213+
// it might have been a mistake. Technically you can make this mistake with
1214+
// AsyncGeneratorFunctions and even single-shot AsyncIterables too but it's extra
1215+
// tempting to try to return the value from a generator.
1216+
if (iterator === iterableChild) {
1217+
const isGeneratorComponent =
1218+
// $FlowIgnore[method-unbinding]
1219+
Object.prototype.toString.call(Component) ===
1220+
'[object AsyncGeneratorFunction]' &&
1221+
// $FlowIgnore[method-unbinding]
1222+
Object.prototype.toString.call(iterableChild) ===
1223+
'[object AsyncGenerator]';
1224+
if (!isGeneratorComponent) {
1225+
callWithDebugContextInDEV(request, task, () => {
1226+
console.error(
1227+
'Returning an AsyncIterator from a Server Component is not supported ' +
1228+
'since it cannot be looped over more than once. ',
1229+
);
1230+
});
1231+
}
1232+
}
1233+
}
1234+
return iterator;
1235+
},
1236+
};
1237+
if (__DEV__) {
1238+
(multishot: any)._debugInfo = iterableChild._debugInfo;
1239+
}
1240+
return multishot;
1241+
}
1242+
return result;
1243+
}
1244+
11081245
function renderFunctionComponent<Props>(
11091246
request: Request,
11101247
task: Task,
@@ -1231,123 +1368,9 @@ function renderFunctionComponent<Props>(
12311368
throw null;
12321369
}
12331370

1234-
if (
1235-
typeof result === 'object' &&
1236-
result !== null &&
1237-
!isClientReference(result)
1238-
) {
1239-
if (typeof result.then === 'function') {
1240-
// When the return value is in children position we can resolve it immediately,
1241-
// to its value without a wrapper if it's synchronously available.
1242-
const thenable: Thenable<any> = result;
1243-
if (__DEV__) {
1244-
// If the thenable resolves to an element, then it was in a static position,
1245-
// the return value of a Server Component. That doesn't need further validation
1246-
// of keys. The Server Component itself would have had a key.
1247-
thenable.then(resolvedValue => {
1248-
if (
1249-
typeof resolvedValue === 'object' &&
1250-
resolvedValue !== null &&
1251-
resolvedValue.$$typeof === REACT_ELEMENT_TYPE
1252-
) {
1253-
resolvedValue._store.validated = 1;
1254-
}
1255-
}, voidHandler);
1256-
}
1257-
if (thenable.status === 'fulfilled') {
1258-
return thenable.value;
1259-
}
1260-
// TODO: Once we accept Promises as children on the client, we can just return
1261-
// the thenable here.
1262-
result = createLazyWrapperAroundWakeable(result);
1263-
}
1371+
// Apply special cases.
1372+
result = processServerComponentReturnValue(request, task, Component, result);
12641373

1265-
// Normally we'd serialize an Iterator/AsyncIterator as a single-shot which is not compatible
1266-
// to be rendered as a React Child. However, because we have the function to recreate
1267-
// an iterable from rendering the element again, we can effectively treat it as multi-
1268-
// shot. Therefore we treat this as an Iterable/AsyncIterable, whether it was one or not, by
1269-
// adding a wrapper so that this component effectively renders down to an AsyncIterable.
1270-
const iteratorFn = getIteratorFn(result);
1271-
if (iteratorFn) {
1272-
const iterableChild = result;
1273-
result = {
1274-
[Symbol.iterator]: function () {
1275-
const iterator = iteratorFn.call(iterableChild);
1276-
if (__DEV__) {
1277-
// If this was an Iterator but not a GeneratorFunction we warn because
1278-
// it might have been a mistake. Technically you can make this mistake with
1279-
// GeneratorFunctions and even single-shot Iterables too but it's extra
1280-
// tempting to try to return the value from a generator.
1281-
if (iterator === iterableChild) {
1282-
const isGeneratorComponent =
1283-
// $FlowIgnore[method-unbinding]
1284-
Object.prototype.toString.call(Component) ===
1285-
'[object GeneratorFunction]' &&
1286-
// $FlowIgnore[method-unbinding]
1287-
Object.prototype.toString.call(iterableChild) ===
1288-
'[object Generator]';
1289-
if (!isGeneratorComponent) {
1290-
callWithDebugContextInDEV(request, task, () => {
1291-
console.error(
1292-
'Returning an Iterator from a Server Component is not supported ' +
1293-
'since it cannot be looped over more than once. ',
1294-
);
1295-
});
1296-
}
1297-
}
1298-
}
1299-
return (iterator: any);
1300-
},
1301-
};
1302-
if (__DEV__) {
1303-
(result: any)._debugInfo = iterableChild._debugInfo;
1304-
}
1305-
} else if (
1306-
enableFlightReadableStream &&
1307-
typeof (result: any)[ASYNC_ITERATOR] === 'function' &&
1308-
(typeof ReadableStream !== 'function' ||
1309-
!(result instanceof ReadableStream))
1310-
) {
1311-
const iterableChild = result;
1312-
result = {
1313-
[ASYNC_ITERATOR]: function () {
1314-
const iterator = (iterableChild: any)[ASYNC_ITERATOR]();
1315-
if (__DEV__) {
1316-
// If this was an AsyncIterator but not an AsyncGeneratorFunction we warn because
1317-
// it might have been a mistake. Technically you can make this mistake with
1318-
// AsyncGeneratorFunctions and even single-shot AsyncIterables too but it's extra
1319-
// tempting to try to return the value from a generator.
1320-
if (iterator === iterableChild) {
1321-
const isGeneratorComponent =
1322-
// $FlowIgnore[method-unbinding]
1323-
Object.prototype.toString.call(Component) ===
1324-
'[object AsyncGeneratorFunction]' &&
1325-
// $FlowIgnore[method-unbinding]
1326-
Object.prototype.toString.call(iterableChild) ===
1327-
'[object AsyncGenerator]';
1328-
if (!isGeneratorComponent) {
1329-
callWithDebugContextInDEV(request, task, () => {
1330-
console.error(
1331-
'Returning an AsyncIterator from a Server Component is not supported ' +
1332-
'since it cannot be looped over more than once. ',
1333-
);
1334-
});
1335-
}
1336-
}
1337-
}
1338-
return iterator;
1339-
},
1340-
};
1341-
if (__DEV__) {
1342-
(result: any)._debugInfo = iterableChild._debugInfo;
1343-
}
1344-
} else if (__DEV__ && (result: any).$$typeof === REACT_ELEMENT_TYPE) {
1345-
// If the server component renders to an element, then it was in a static position.
1346-
// That doesn't need further validation of keys. The Server Component itself would
1347-
// have had a key.
1348-
(result: any)._store.validated = 1;
1349-
}
1350-
}
13511374
// Track this element's key on the Server Component on the keyPath context..
13521375
const prevKeyPath = task.keyPath;
13531376
const prevImplicitSlot = task.implicitSlot;

0 commit comments

Comments
 (0)