[Flight] Extract special cases for Server Component return value position (#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.
Sebastian Markbåge committed
Dec 9, 2024 at 17:20 UTC
3d2ab01a559943b3f041f841dc0d796d92be0a87
1 file changed
+139
-116
packages/react-server/src/ReactFlightServer.js
+139
-116
@@ -1105,6 +1105,143 @@ function callWithDebugContextInDEV<A, T>(
1105
1106
const voidHandler = () => {};
1107
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
+
1245
function renderFunctionComponent<Props>(
1246
request: Request,
1247
task: Task,
@@ -1231,123 +1368,9 @@ function renderFunctionComponent<Props>(
1368
throw null;
1369
}
1370
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);
1373
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
- }
1374
// Track this element's key on the Server Component on the keyPath context..
1375
const prevKeyPath = task.keyPath;
1376
const prevImplicitSlot = task.implicitSlot;