Add tail="collapsed" option to SuspenseList (#16007)
* Add tail="collapsed" option * Fix issue with tail exceeding the CPU time limit We used to assume that this didn't suspend but this branch happens in both cases. This fixes it so that we first check if we suspended. Now we can fix the tail so that it always render an additional fallback in this scenario.
Sebastian Markbåge committed
Jul 1, 2019 at 19:56 UTC
5cb8f6f344a6f3e296f095b8d151157d1f05c835
4 files changed
+659
-4
packages/react-reconciler/src/ReactFiberBeginWork.js
+39
@@ -14,6 +14,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
14
import type {
15
SuspenseState,
16
SuspenseListRenderState,
17
+ SuspenseListTailMode,
18
} from './ReactFiberSuspenseComponent';
19
import type {SuspenseContext} from './ReactFiberSuspenseContext';
20
@@ -188,6 +189,7 @@ let didWarnAboutFunctionRefs;
189
export let didWarnAboutReassigningProps;
190
let didWarnAboutMaxDuration;
191
let didWarnAboutRevealOrder;
192
+let didWarnAboutTailOptions;
193
194
if (__DEV__) {
195
didWarnAboutBadClass = {};
@@ -198,6 +200,7 @@ if (__DEV__) {
200
didWarnAboutReassigningProps = false;
201
didWarnAboutMaxDuration = false;
202
didWarnAboutRevealOrder = {};
203
+ didWarnAboutTailOptions = {};
204
}
205
206
export function reconcileChildren(
@@ -2063,11 +2066,40 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
2066
}
2067
}
2068
2069
+function validateTailOptions(
2070
+ tailMode: SuspenseListTailMode,
2071
+ revealOrder: SuspenseListRevealOrder,
2072
+) {
2073
+ if (__DEV__) {
2074
+ if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) {
2075
+ if (tailMode !== 'collapsed') {
2076
+ didWarnAboutTailOptions[tailMode] = true;
2077
+ warning(
2078
+ false,
2079
+ '"%s" is not a supported value for tail on <SuspenseList />. ' +
2080
+ 'Did you mean "collapsed"?',
2081
+ tailMode,
2082
+ );
2083
+ } else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') {
2084
+ didWarnAboutTailOptions[tailMode] = true;
2085
+ warning(
2086
+ false,
2087
+ '<SuspenseList tail="%s" /> is only valid if revealOrder is ' +
2088
+ '"forwards" or "backwards". ' +
2089
+ 'Did you mean to specify revealOrder="forwards"?',
2090
+ tailMode,
2091
+ );
2092
+ }
2093
+ }
2094
+ }
2095
+}
2096
+
2097
function initSuspenseListRenderState(
2098
workInProgress: Fiber,
2099
isBackwards: boolean,
2100
tail: null | Fiber,
2101
lastContentRow: null | Fiber,
2102
+ tailMode: SuspenseListTailMode,
2103
): void {
2104
let renderState: null | SuspenseListRenderState =
2105
workInProgress.memoizedState;
@@ -2078,6 +2110,7 @@ function initSuspenseListRenderState(
2110
last: lastContentRow,
2111
tail: tail,
2112
tailExpiration: 0,
2113
+ tailMode: tailMode,
2114
};
2115
} else {
2116
// We can reuse the existing object from previous renders.
@@ -2086,6 +2119,7 @@ function initSuspenseListRenderState(
2119
renderState.last = lastContentRow;
2120
renderState.tail = tail;
2121
renderState.tailExpiration = 0;
2122
+ renderState.tailMode = tailMode;
2123
}
2124
}
2125
@@ -2103,9 +2137,11 @@ function updateSuspenseListComponent(
2137
) {
2138
const nextProps = workInProgress.pendingProps;
2139
const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
2140
+ const tailMode: SuspenseListTailMode = nextProps.tail;
2141
const newChildren = nextProps.children;
2142
2143
validateRevealOrder(revealOrder);
2144
+ validateTailOptions(tailMode, revealOrder);
2145
2146
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
2147
@@ -2163,6 +2199,7 @@ function updateSuspenseListComponent(
2199
false, // isBackwards
2200
tail,
2201
lastContentRow,
2202
+ tailMode,
2203
);
2204
break;
2205
}
@@ -2193,6 +2230,7 @@ function updateSuspenseListComponent(
2230
true, // isBackwards
2231
tail,
2232
null, // last
2233
+ tailMode,
2234
);
2235
break;
2236
}
@@ -2202,6 +2240,7 @@ function updateSuspenseListComponent(
2240
false, // isBackwards
2241
null, // tail
2242
null, // last
2243
+ undefined,
2244
);
2245
break;
2246
}
packages/react-reconciler/src/ReactFiberCompleteWork.js
+51
-4
@@ -537,6 +537,46 @@ if (supportsMutation) {
537
};
538
}
539
540
+function cutOffTailIfNeeded(
541
+ renderState: SuspenseListRenderState,
542
+ hasRenderedATailFallback: boolean,
543
+) {
544
+ switch (renderState.tailMode) {
545
+ case 'collapsed': {
546
+ // Any insertions at the end of the tail list after this point
547
+ // should be invisible. If there are already mounted boundaries
548
+ // anything before them are not considered for collapsing.
549
+ // Therefore we need to go through the whole tail to find if
550
+ // there are any.
551
+ let tailNode = renderState.tail;
552
+ let lastTailNode = null;
553
+ while (tailNode !== null) {
554
+ if (tailNode.alternate !== null) {
555
+ lastTailNode = tailNode;
556
+ }
557
+ tailNode = tailNode.sibling;
558
+ }
559
+ // Next we're simply going to delete all insertions after the
560
+ // last rendered item.
561
+ if (lastTailNode === null) {
562
+ // All remaining items in the tail are insertions.
563
+ if (!hasRenderedATailFallback && renderState.tail !== null) {
564
+ // We suspended during the head. We want to show at least one
565
+ // row at the tail. So we'll keep on and cut off the rest.
566
+ renderState.tail.sibling = null;
567
+ } else {
568
+ renderState.tail = null;
569
+ }
570
+ } else {
571
+ // Detach the insertion after the last node that was already
572
+ // inserted.
573
+ lastTailNode.sibling = null;
574
+ }
575
+ break;
576
+ }
577
+ }
578
+}
579
+
580
// Note this, might mutate the workInProgress passed in.
581
function hasSuspendedChildrenAndNewContent(
582
workInProgress: Fiber,
@@ -991,11 +1031,18 @@ function completeWork(
1031
didSuspendAlready =
1032
(workInProgress.effectTag & DidCapture) !== NoEffect;
1033
}
1034
+ if (didSuspendAlready) {
1035
+ cutOffTailIfNeeded(renderState, false);
1036
+ }
1037
// Next we're going to render the tail.
1038
} else {
1039
// Append the rendered row to the child list.
1040
if (!didSuspendAlready) {
998
- if (
1041
+ if (isShowingAnyFallbacks(renderedTail)) {
1042
+ workInProgress.effectTag |= DidCapture;
1043
+ didSuspendAlready = true;
1044
+ cutOffTailIfNeeded(renderState, true);
1045
+ } else if (
1046
now() > renderState.tailExpiration &&
1047
renderExpirationTime > Never
1048
) {
@@ -1004,6 +1051,9 @@ function completeWork(
1051
// The assumption is that this is usually faster.
1052
workInProgress.effectTag |= DidCapture;
1053
didSuspendAlready = true;
1054
+
1055
+ cutOffTailIfNeeded(renderState, false);
1056
+
1057
// Since nothing actually suspended, there will nothing to ping this
1058
// to get it started back up to attempt the next item. If we can show
1059
// them, then they really have the same priority as this render.
@@ -1015,9 +1065,6 @@ function completeWork(
1065
if (enableSchedulerTracing) {
1066
markSpawnedWork(nextPriority);
1067
}
1018
- } else if (isShowingAnyFallbacks(renderedTail)) {
1019
- workInProgress.effectTag |= DidCapture;
1020
- didSuspendAlready = true;
1068
}
1069
}
1070
if (renderState.isBackwards) {
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+4
@@ -14,6 +14,8 @@ import {SuspenseComponent} from 'shared/ReactWorkTags';
14
// Alternatively we can make this use an effect tag similar to SuspenseList.
15
export type SuspenseState = {||};
16
17
+export type SuspenseListTailMode = 'collapsed' | void;
18
+
19
export type SuspenseListRenderState = {|
20
isBackwards: boolean,
21
// The currently rendering tail row.
@@ -24,6 +26,8 @@ export type SuspenseListRenderState = {|
26
tail: null | Fiber,
27
// The absolute time in ms that we'll expire the tail rendering.
28
tailExpiration: number,
29
+ // Tail insertions setting.
30
+ tailMode: SuspenseListTailMode,
31
|};
32
33
export function shouldCaptureSuspense(
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+565
@@ -1088,4 +1088,569 @@ describe('ReactSuspenseList', () => {
1088
</Fragment>,
1089
);
1090
});
1091
+
1092
+ it('only shows one loading state at a time for "collapsed" tail insertions', async () => {
1093
+ let A = createAsyncText('A');
1094
+ let B = createAsyncText('B');
1095
+ let C = createAsyncText('C');
1096
+
1097
+ function Foo() {
1098
+ return (
1099
+ <SuspenseList revealOrder="forwards" tail="collapsed">
1100
+ <Suspense fallback={<Text text="Loading A" />}>
1101
+ <A />
1102
+ </Suspense>
1103
+ <Suspense fallback={<Text text="Loading B" />}>
1104
+ <B />
1105
+ </Suspense>
1106
+ <Suspense fallback={<Text text="Loading C" />}>
1107
+ <C />
1108
+ </Suspense>
1109
+ </SuspenseList>
1110
+ );
1111
+ }
1112
+
1113
+ ReactNoop.render(<Foo />);
1114
+
1115
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A']);
1116
+
1117
+ expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
1118
+
1119
+ await A.resolve();
1120
+
1121
+ expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'Loading B']);
1122
+
1123
+ // Incremental loading is suspended.
1124
+ jest.advanceTimersByTime(500);
1125
+
1126
+ expect(ReactNoop).toMatchRenderedOutput(
1127
+ <Fragment>
1128
+ <span>A</span>
1129
+ <span>Loading B</span>
1130
+ </Fragment>,
1131
+ );
1132
+
1133
+ await B.resolve();
1134
+
1135
+ expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]', 'Loading C']);
1136
+
1137
+ // Incremental loading is suspended.
1138
+ jest.advanceTimersByTime(500);
1139
+
1140
+ expect(ReactNoop).toMatchRenderedOutput(
1141
+ <Fragment>
1142
+ <span>A</span>
1143
+ <span>B</span>
1144
+ <span>Loading C</span>
1145
+ </Fragment>,
1146
+ );
1147
+
1148
+ await C.resolve();
1149
+
1150
+ expect(Scheduler).toFlushAndYield(['C']);
1151
+
1152
+ expect(ReactNoop).toMatchRenderedOutput(
1153
+ <Fragment>
1154
+ <span>A</span>
1155
+ <span>B</span>
1156
+ <span>C</span>
1157
+ </Fragment>,
1158
+ );
1159
+ });
1160
+
1161
+ it('warns if an unsupported tail option is used', () => {
1162
+ function Foo() {
1163
+ return (
1164
+ <SuspenseList revealOrder="forwards" tail="collapse">
1165
+ <Suspense fallback="Loading">Content</Suspense>
1166
+ </SuspenseList>
1167
+ );
1168
+ }
1169
+
1170
+ ReactNoop.render(<Foo />);
1171
+
1172
+ expect(() => Scheduler.unstable_flushAll()).toWarnDev([
1173
+ 'Warning: "collapse" is not a supported value for tail on ' +
1174
+ '<SuspenseList />. Did you mean "collapsed"?' +
1175
+ '\n in SuspenseList (at **)' +
1176
+ '\n in Foo (at **)',
1177
+ ]);
1178
+ });
1179
+
1180
+ it('warns if a tail option is used with "together"', () => {
1181
+ function Foo() {
1182
+ return (
1183
+ <SuspenseList revealOrder="together" tail="collapsed">
1184
+ <Suspense fallback="Loading">Content</Suspense>
1185
+ </SuspenseList>
1186
+ );
1187
+ }
1188
+
1189
+ ReactNoop.render(<Foo />);
1190
+
1191
+ expect(() => Scheduler.unstable_flushAll()).toWarnDev([
1192
+ 'Warning: <SuspenseList tail="collapsed" /> is only valid if ' +
1193
+ 'revealOrder is "forwards" or "backwards". ' +
1194
+ 'Did you mean to specify revealOrder="forwards"?' +
1195
+ '\n in SuspenseList (at **)' +
1196
+ '\n in Foo (at **)',
1197
+ ]);
1198
+ });
1199
+
1200
+ it('renders one "collapsed" fallback even if CPU time elapsed', async () => {
1201
+ function Foo() {
1202
+ return (
1203
+ <SuspenseList revealOrder="forwards" tail="collapsed">
1204
+ <Suspense fallback={<Text text="Loading A" />}>
1205
+ <Text text="A" />
1206
+ </Suspense>
1207
+ <Suspense fallback={<Text text="Loading B" />}>
1208
+ <Text text="B" />
1209
+ </Suspense>
1210
+ <Suspense fallback={<Text text="Loading C" />}>
1211
+ <Text text="C" />
1212
+ </Suspense>
1213
+ <Suspense fallback={<Text text="Loading D" />}>
1214
+ <Text text="D" />
1215
+ </Suspense>
1216
+ </SuspenseList>
1217
+ );
1218
+ }
1219
+
1220
+ // This render is only CPU bound. Nothing suspends.
1221
+ ReactNoop.render(<Foo />);
1222
+
1223
+ expect(Scheduler).toFlushAndYieldThrough(['A']);
1224
+
1225
+ Scheduler.unstable_advanceTime(300);
1226
+ jest.advanceTimersByTime(300);
1227
+
1228
+ expect(Scheduler).toFlushAndYieldThrough(['B']);
1229
+
1230
+ Scheduler.unstable_advanceTime(300);
1231
+ jest.advanceTimersByTime(300);
1232
+
1233
+ // We've still not been able to show anything on the screen even though
1234
+ // we have two items ready.
1235
+ expect(ReactNoop).toMatchRenderedOutput(null);
1236
+
1237
+ // Time has now elapsed for so long that we're just going to give up
1238
+ // rendering the rest of the content. So that we can at least show
1239
+ // something.
1240
+ expect(Scheduler).toFlushAndYieldThrough([
1241
+ 'Loading C',
1242
+ 'C', // I'll flush through into the next render so that the first commits.
1243
+ ]);
1244
+
1245
+ expect(ReactNoop).toMatchRenderedOutput(
1246
+ <Fragment>
1247
+ <span>A</span>
1248
+ <span>B</span>
1249
+ <span>Loading C</span>
1250
+ </Fragment>,
1251
+ );
1252
+
1253
+ // Then we do a second pass to commit the last two items.
1254
+ expect(Scheduler).toFlushAndYield(['D']);
1255
+
1256
+ expect(ReactNoop).toMatchRenderedOutput(
1257
+ <Fragment>
1258
+ <span>A</span>
1259
+ <span>B</span>
1260
+ <span>C</span>
1261
+ <span>D</span>
1262
+ </Fragment>,
1263
+ );
1264
+ });
1265
+
1266
+ it('adding to the middle does not collapse insertions (forwards)', async () => {
1267
+ let A = createAsyncText('A');
1268
+ let B = createAsyncText('B');
1269
+ let C = createAsyncText('C');
1270
+ let D = createAsyncText('D');
1271
+ let E = createAsyncText('E');
1272
+ let F = createAsyncText('F');
1273
+
1274
+ function Foo({items}) {
1275
+ return (
1276
+ <SuspenseList revealOrder="forwards" tail="collapsed">
1277
+ {items.map(([key, Component]) => (
1278
+ <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1279
+ <Component />
1280
+ </Suspense>
1281
+ ))}
1282
+ </SuspenseList>
1283
+ );
1284
+ }
1285
+
1286
+ ReactNoop.render(<Foo items={[['A', A], ['D', D]]} />);
1287
+
1288
+ await A.resolve();
1289
+ await D.resolve();
1290
+
1291
+ expect(Scheduler).toFlushAndYield(['A', 'D']);
1292
+
1293
+ // First render commits A and D.
1294
+ expect(ReactNoop).toMatchRenderedOutput(
1295
+ <Fragment>
1296
+ <span>A</span>
1297
+ <span>D</span>
1298
+ </Fragment>,
1299
+ );
1300
+
1301
+ // For the second render, we're going to insert items in the middle and end.
1302
+ ReactNoop.render(
1303
+ <Foo
1304
+ items={[['A', A], ['B', B], ['C', C], ['D', D], ['E', E], ['F', F]]}
1305
+ />,
1306
+ );
1307
+
1308
+ expect(Scheduler).toFlushAndYield([
1309
+ 'A',
1310
+ 'Suspend! [B]',
1311
+ 'Loading B',
1312
+ 'Suspend! [C]',
1313
+ 'Loading C',
1314
+ 'D',
1315
+ 'Loading E',
1316
+ ]);
1317
+
1318
+ // B and C don't get collapsed, but F gets collapsed with E.
1319
+ expect(ReactNoop).toMatchRenderedOutput(
1320
+ <Fragment>
1321
+ <span>A</span>
1322
+ <span>Loading B</span>
1323
+ <span>Loading C</span>
1324
+ <span>D</span>
1325
+ <span>Loading E</span>
1326
+ </Fragment>,
1327
+ );
1328
+
1329
+ await B.resolve();
1330
+
1331
+ expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]']);
1332
+
1333
+ // Incremental loading is suspended.
1334
+ jest.advanceTimersByTime(500);
1335
+
1336
+ // Even though B is unsuspended, it's still in loading state because
1337
+ // it is blocked by C.
1338
+ expect(ReactNoop).toMatchRenderedOutput(
1339
+ <Fragment>
1340
+ <span>A</span>
1341
+ <span>Loading B</span>
1342
+ <span>Loading C</span>
1343
+ <span>D</span>
1344
+ <span>Loading E</span>
1345
+ </Fragment>,
1346
+ );
1347
+
1348
+ await C.resolve();
1349
+ await E.resolve();
1350
+
1351
+ expect(Scheduler).toFlushAndYield([
1352
+ 'B',
1353
+ 'C',
1354
+ 'E',
1355
+ 'Suspend! [F]',
1356
+ 'Loading F',
1357
+ ]);
1358
+
1359
+ jest.advanceTimersByTime(500);
1360
+
1361
+ expect(ReactNoop).toMatchRenderedOutput(
1362
+ <Fragment>
1363
+ <span>A</span>
1364
+ <span>B</span>
1365
+ <span>C</span>
1366
+ <span>D</span>
1367
+ <span>E</span>
1368
+ <span>Loading F</span>
1369
+ </Fragment>,
1370
+ );
1371
+
1372
+ await F.resolve();
1373
+
1374
+ expect(Scheduler).toFlushAndYield(['F']);
1375
+
1376
+ jest.advanceTimersByTime(500);
1377
+
1378
+ expect(ReactNoop).toMatchRenderedOutput(
1379
+ <Fragment>
1380
+ <span>A</span>
1381
+ <span>B</span>
1382
+ <span>C</span>
1383
+ <span>D</span>
1384
+ <span>E</span>
1385
+ <span>F</span>
1386
+ </Fragment>,
1387
+ );
1388
+ });
1389
+
1390
+ it('adding to the middle does not collapse insertions (backwards)', async () => {
1391
+ let A = createAsyncText('A');
1392
+ let B = createAsyncText('B');
1393
+ let C = createAsyncText('C');
1394
+ let D = createAsyncText('D');
1395
+ let E = createAsyncText('E');
1396
+ let F = createAsyncText('F');
1397
+
1398
+ function Foo({items}) {
1399
+ return (
1400
+ <SuspenseList revealOrder="backwards" tail="collapsed">
1401
+ {items.map(([key, Component]) => (
1402
+ <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1403
+ <Component />
1404
+ </Suspense>
1405
+ ))}
1406
+ </SuspenseList>
1407
+ );
1408
+ }
1409
+
1410
+ ReactNoop.render(<Foo items={[['C', C], ['F', F]]} />);
1411
+
1412
+ await C.resolve();
1413
+ await F.resolve();
1414
+
1415
+ expect(Scheduler).toFlushAndYield(['F', 'C']);
1416
+
1417
+ // First render commits C and F.
1418
+ expect(ReactNoop).toMatchRenderedOutput(
1419
+ <Fragment>
1420
+ <span>C</span>
1421
+ <span>F</span>
1422
+ </Fragment>,
1423
+ );
1424
+
1425
+ // For the second render, we're going to insert items in the middle and end.
1426
+ ReactNoop.render(
1427
+ <Foo
1428
+ items={[['A', A], ['B', B], ['C', C], ['D', D], ['E', E], ['F', F]]}
1429
+ />,
1430
+ );
1431
+
1432
+ expect(Scheduler).toFlushAndYield([
1433
+ 'C',
1434
+ 'Suspend! [D]',
1435
+ 'Loading D',
1436
+ 'Suspend! [E]',
1437
+ 'Loading E',
1438
+ 'F',
1439
+ 'Loading B',
1440
+ ]);
1441
+
1442
+ // D and E don't get collapsed, but A gets collapsed with B.
1443
+ expect(ReactNoop).toMatchRenderedOutput(
1444
+ <Fragment>
1445
+ <span>Loading B</span>
1446
+ <span>C</span>
1447
+ <span>Loading D</span>
1448
+ <span>Loading E</span>
1449
+ <span>F</span>
1450
+ </Fragment>,
1451
+ );
1452
+
1453
+ await E.resolve();
1454
+
1455
+ expect(Scheduler).toFlushAndYield(['Suspend! [D]', 'E']);
1456
+
1457
+ // Incremental loading is suspended.
1458
+ jest.advanceTimersByTime(500);
1459
+
1460
+ // Even though E is unsuspended, it's still in loading state because
1461
+ // it is blocked by D.
1462
+ expect(ReactNoop).toMatchRenderedOutput(
1463
+ <Fragment>
1464
+ <span>Loading B</span>
1465
+ <span>C</span>
1466
+ <span>Loading D</span>
1467
+ <span>Loading E</span>
1468
+ <span>F</span>
1469
+ </Fragment>,
1470
+ );
1471
+
1472
+ await C.resolve();
1473
+ await E.resolve();
1474
+
1475
+ await B.resolve();
1476
+ await C.resolve();
1477
+ await D.resolve();
1478
+ await E.resolve();
1479
+
1480
+ expect(Scheduler).toFlushAndYield([
1481
+ 'D',
1482
+ 'E',
1483
+ 'B',
1484
+ 'Suspend! [A]',
1485
+ 'Loading A',
1486
+ ]);
1487
+
1488
+ jest.advanceTimersByTime(500);
1489
+
1490
+ expect(ReactNoop).toMatchRenderedOutput(
1491
+ <Fragment>
1492
+ <span>Loading A</span>
1493
+ <span>B</span>
1494
+ <span>C</span>
1495
+ <span>D</span>
1496
+ <span>E</span>
1497
+ <span>F</span>
1498
+ </Fragment>,
1499
+ );
1500
+
1501
+ await A.resolve();
1502
+
1503
+ expect(Scheduler).toFlushAndYield(['A']);
1504
+
1505
+ jest.advanceTimersByTime(500);
1506
+
1507
+ expect(ReactNoop).toMatchRenderedOutput(
1508
+ <Fragment>
1509
+ <span>A</span>
1510
+ <span>B</span>
1511
+ <span>C</span>
1512
+ <span>D</span>
1513
+ <span>E</span>
1514
+ <span>F</span>
1515
+ </Fragment>,
1516
+ );
1517
+ });
1518
+
1519
+ it('adding to the middle of committeed tail does not collapse insertions', async () => {
1520
+ let A = createAsyncText('A');
1521
+ let B = createAsyncText('B');
1522
+ let C = createAsyncText('C');
1523
+ let D = createAsyncText('D');
1524
+ let E = createAsyncText('E');
1525
+ let F = createAsyncText('F');
1526
+
1527
+ function SyncD() {
1528
+ return <Text text="D" />;
1529
+ }
1530
+
1531
+ function Foo({items}) {
1532
+ return (
1533
+ <SuspenseList revealOrder="forwards" tail="collapsed">
1534
+ {items.map(([key, Component]) => (
1535
+ <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1536
+ <Component />
1537
+ </Suspense>
1538
+ ))}
1539
+ </SuspenseList>
1540
+ );
1541
+ }
1542
+
1543
+ ReactNoop.render(<Foo items={[['A', A], ['D', SyncD]]} />);
1544
+
1545
+ await A.resolve();
1546
+
1547
+ expect(Scheduler).toFlushAndYield(['A', 'D']);
1548
+
1549
+ // First render commits A and D.
1550
+ expect(ReactNoop).toMatchRenderedOutput(
1551
+ <Fragment>
1552
+ <span>A</span>
1553
+ <span>D</span>
1554
+ </Fragment>,
1555
+ );
1556
+
1557
+ // For the second render, we're going to insert items in the middle and end.
1558
+ // Note that D now suspends even though it didn't in the first pass.
1559
+ ReactNoop.render(
1560
+ <Foo
1561
+ items={[['A', A], ['B', B], ['C', C], ['D', D], ['E', E], ['F', F]]}
1562
+ />,
1563
+ );
1564
+
1565
+ expect(Scheduler).toFlushAndYield([
1566
+ 'A',
1567
+ 'Suspend! [B]',
1568
+ 'Loading B',
1569
+ 'Suspend! [C]',
1570
+ 'Loading C',
1571
+ 'Suspend! [D]',
1572
+ 'Loading D',
1573
+ 'Loading E',
1574
+ ]);
1575
+
1576
+ // This is suspended due to the update to D causing a loading state.
1577
+ jest.advanceTimersByTime(500);
1578
+
1579
+ // B and C don't get collapsed, but F gets collapsed with E.
1580
+ // Even though everything in the bottom of the list is suspended, we don't
1581
+ // collapse them because D was an update. Not an insertion.
1582
+ expect(ReactNoop).toMatchRenderedOutput(
1583
+ <Fragment>
1584
+ <span>A</span>
1585
+ <span>Loading B</span>
1586
+ <span>Loading C</span>
1587
+ <span hidden={true}>D</span>
1588
+ <span>Loading D</span>
1589
+ <span>Loading E</span>
1590
+ </Fragment>,
1591
+ );
1592
+
1593
+ await B.resolve();
1594
+
1595
+ expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]']);
1596
+
1597
+ // Incremental loading is suspended.
1598
+ jest.advanceTimersByTime(500);
1599
+
1600
+ // B is able to unblock here because it's part of the tail.
1601
+ // If D was still visible it wouldn't be part of the tail
1602
+ // and would be blocked on C like in the other test.
1603
+ expect(ReactNoop).toMatchRenderedOutput(
1604
+ <Fragment>
1605
+ <span>A</span>
1606
+ <span>B</span>
1607
+ <span>Loading C</span>
1608
+ <span hidden={true}>D</span>
1609
+ <span>Loading D</span>
1610
+ <span>Loading E</span>
1611
+ </Fragment>,
1612
+ );
1613
+
1614
+ await C.resolve();
1615
+ await D.resolve();
1616
+ await E.resolve();
1617
+
1618
+ expect(Scheduler).toFlushAndYield([
1619
+ 'C',
1620
+ 'D',
1621
+ 'E',
1622
+ 'Suspend! [F]',
1623
+ 'Loading F',
1624
+ ]);
1625
+
1626
+ jest.advanceTimersByTime(500);
1627
+
1628
+ expect(ReactNoop).toMatchRenderedOutput(
1629
+ <Fragment>
1630
+ <span>A</span>
1631
+ <span>B</span>
1632
+ <span>C</span>
1633
+ <span>D</span>
1634
+ <span>E</span>
1635
+ <span>Loading F</span>
1636
+ </Fragment>,
1637
+ );
1638
+
1639
+ await F.resolve();
1640
+
1641
+ expect(Scheduler).toFlushAndYield(['F']);
1642
+
1643
+ jest.advanceTimersByTime(500);
1644
+
1645
+ expect(ReactNoop).toMatchRenderedOutput(
1646
+ <Fragment>
1647
+ <span>A</span>
1648
+ <span>B</span>
1649
+ <span>C</span>
1650
+ <span>D</span>
1651
+ <span>E</span>
1652
+ <span>F</span>
1653
+ </Fragment>,
1654
+ );
1655
+ });
1656
});