@samitouri / QOS-React-2 / commits / 3d2ab01a55

[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;