1270
assertLog(['Loading... (25%)', 'A', 'B']);
1271
expect(root).toMatchRenderedOutput(<div>B</div>);
1272
});
1273
+
1274
+ // @gate enableAsyncActions
1275
+ test(
1276
+ 'optimistic state is not reverted until async action finishes, even if ' +
1277
+ 'useTransition hook is unmounted',
1278
+ async () => {
1279
+ let startTransition;
1280
+ function Updater() {
1281
+ const [isPending, _start] = useTransition();
1282
+ startTransition = _start;
1283
+ return (
1284
+ <span>
1285
+ <Text text={'Pending: ' + isPending} />
1286
+ </span>
1287
+ );
1288
+ }
1289
+
1290
+ let setText;
1291
+ let setOptimisticText;
1292
+ function Sibling() {
1293
+ const [canonicalText, _setText] = useState('A');
1294
+ setText = _setText;
1295
+
1296
+ const [text, _setOptimisticText] = useOptimistic(
1297
+ canonicalText,
1298
+ (_, optimisticText) => `${optimisticText} (loading...)`,
1299
+ );
1300
+ setOptimisticText = _setOptimisticText;
1301
+
1302
+ return (
1303
+ <span>
1304
+ <Text text={text} />
1305
+ </span>
1306
+ );
1307
+ }
1308
+
1309
+ function App({showUpdater}) {
1310
+ return (
1311
+ <>
1312
+ {showUpdater ? <Updater /> : null}
1313
+ <Sibling />
1314
+ </>
1315
+ );
1316
+ }
1317
+
1318
+ const root = ReactNoop.createRoot();
1319
+ await act(() => {
1320
+ root.render(<App showUpdater={true} />);
1321
+ });
1322
+ assertLog(['Pending: false', 'A']);
1323
+ expect(root).toMatchRenderedOutput(
1324
+ <>
1325
+ <span>Pending: false</span>
1326
+ <span>A</span>
1327
+ </>,
1328
+ );
1329
+
1330
+ // Start an async action that has multiple updates with async
1331
+ // operations in between.
1332
+ await act(() => {
1333
+ startTransition(async () => {
1334
+ Scheduler.log('Async action started');
1335
+
1336
+ setOptimisticText('C');
1337
+
1338
+ startTransition(() => setText('B'));
1339
+
1340
+ await getText('Wait before updating to C');
1341
+
1342
+ Scheduler.log('Async action ended');
1343
+ startTransition(() => setText('C'));
1344
+ });
1345
+ });
1346
+ assertLog([
1347
+ 'Async action started',
1348
+ 'Pending: true',
1349
+ // Render an optimistic value
1350
+ 'C (loading...)',
1351
+ ]);
1352
+ expect(root).toMatchRenderedOutput(
1353
+ <>
1354
+ <span>Pending: true</span>
1355
+ <span>C (loading...)</span>
1356
+ </>,
1357
+ );
1358
+
1359
+ // Delete the component that contains the useTransition hook. This
1360
+ // component no longer blocks the transition from completing. But the
1361
+ // we're still showing an optimistic state, because the async action has
1362
+ // not yet finished.
1363
+ await act(() => {
1364
+ root.render(<App showUpdater={false} />);
1365
+ });
1366
+ assertLog(['C (loading...)']);
1367
+ expect(root).toMatchRenderedOutput(<span>C (loading...)</span>);
1368
+
1369
+ // Finish the async action. Now the optimistic state is reverted and we
1370
+ // switch to the canonical value.
1371
+ await act(() => resolveText('Wait before updating to C'));
1372
+ assertLog(['Async action ended', 'C']);
1373
+ expect(root).toMatchRenderedOutput(<span>C</span>);
1374
+ },
1375
+ );
1376
+
1377
+ // @gate enableAsyncActions
1378
+ test(
1379
+ 'updates in an async action are entangled even if useTransition hook ' +
1380
+ 'is unmounted before it finishes',
1381
+ async () => {
1382
+ let startTransition;
1383
+ function Updater() {
1384
+ const [isPending, _start] = useTransition();
1385
+ startTransition = _start;
1386
+ return (
1387
+ <span>
1388
+ <Text text={'Pending: ' + isPending} />
1389
+ </span>
1390
+ );
1391
+ }
1392
+
1393
+ let setText;
1394
+ function Sibling() {
1395
+ const [text, _setText] = useState('A');
1396
+ setText = _setText;
1397
+ return (
1398
+ <span>
1399
+ <Text text={text} />
1400
+ </span>
1401
+ );
1402
+ }
1403
+
1404
+ function App({showUpdater}) {
1405
+ return (
1406
+ <>
1407
+ {showUpdater ? <Updater /> : null}
1408
+ <Sibling />
1409
+ </>
1410
+ );
1411
+ }
1412
+
1413
+ const root = ReactNoop.createRoot();
1414
+ await act(() => {
1415
+ root.render(<App showUpdater={true} />);
1416
+ });
1417
+ assertLog(['Pending: false', 'A']);
1418
+ expect(root).toMatchRenderedOutput(
1419
+ <>
1420
+ <span>Pending: false</span>
1421
+ <span>A</span>
1422
+ </>,
1423
+ );
1424
+
1425
+ // Start an async action that has multiple updates with async
1426
+ // operations in between.
1427
+ await act(() => {
1428
+ startTransition(async () => {
1429
+ Scheduler.log('Async action started');
1430
+ startTransition(() => setText('B'));
1431
+
1432
+ await getText('Wait before updating to C');
1433
+
1434
+ Scheduler.log('Async action ended');
1435
+ startTransition(() => setText('C'));
1436
+ });
1437
+ });
1438
+ assertLog(['Async action started', 'Pending: true']);
1439
+ expect(root).toMatchRenderedOutput(
1440
+ <>
1441
+ <span>Pending: true</span>
1442
+ <span>A</span>
1443
+ </>,
1444
+ );
1445
+
1446
+ // Delete the component that contains the useTransition hook. This
1447
+ // component no longer blocks the transition from completing. But the
1448
+ // pending update to Sibling should not be allowed to finish, because it's
1449
+ // part of the async action.
1450
+ await act(() => {
1451
+ root.render(<App showUpdater={false} />);
1452
+ });
1453
+ assertLog(['A']);
1454
+ expect(root).toMatchRenderedOutput(<span>A</span>);
1455
+
1456
+ // Finish the async action. Notice the intermediate B state was never
1457
+ // shown, because it was batched with the update that came later in the
1458
+ // same action.
1459
+ await act(() => resolveText('Wait before updating to C'));
1460
+ assertLog(['Async action ended', 'C']);
1461
+ expect(root).toMatchRenderedOutput(<span>C</span>);
1462
+ },
1463
+ );
1464
+
1465
+ // @gate enableAsyncActions
1466
+ test(
1467
+ 'updates in an async action are entangled even if useTransition hook ' +
1468
+ 'is unmounted before it finishes (class component)',
1469
+ async () => {
1470
+ let startTransition;
1471
+ function Updater() {
1472
+ const [isPending, _start] = useTransition();
1473
+ startTransition = _start;
1474
+ return (
1475
+ <span>
1476
+ <Text text={'Pending: ' + isPending} />
1477
+ </span>
1478
+ );
1479
+ }
1480
+
1481
+ let setText;
1482
+ class Sibling extends React.Component {
1483
+ state = {text: 'A'};
1484
+ render() {
1485
+ setText = text => this.setState({text});
1486
+ return (
1487
+ <span>
1488
+ <Text text={this.state.text} />
1489
+ </span>
1490
+ );
1491
+ }
1492
+ }
1493
+
1494
+ function App({showUpdater}) {
1495
+ return (
1496
+ <>
1497
+ {showUpdater ? <Updater /> : null}
1498
+ <Sibling />
1499
+ </>
1500
+ );
1501
+ }
1502
+
1503
+ const root = ReactNoop.createRoot();
1504
+ await act(() => {
1505
+ root.render(<App showUpdater={true} />);
1506
+ });
1507
+ assertLog(['Pending: false', 'A']);
1508
+ expect(root).toMatchRenderedOutput(
1509
+ <>
1510
+ <span>Pending: false</span>
1511
+ <span>A</span>
1512
+ </>,
1513
+ );
1514
+
1515
+ // Start an async action that has multiple updates with async
1516
+ // operations in between.
1517
+ await act(() => {
1518
+ startTransition(async () => {
1519
+ Scheduler.log('Async action started');
1520
+ startTransition(() => setText('B'));
1521
+
1522
+ await getText('Wait before updating to C');
1523
+
1524
+ Scheduler.log('Async action ended');
1525
+ startTransition(() => setText('C'));
1526
+ });
1527
+ });
1528
+ assertLog(['Async action started', 'Pending: true']);
1529
+ expect(root).toMatchRenderedOutput(
1530
+ <>
1531
+ <span>Pending: true</span>
1532
+ <span>A</span>
1533
+ </>,
1534
+ );
1535
+
1536
+ // Delete the component that contains the useTransition hook. This
1537
+ // component no longer blocks the transition from completing. But the
1538
+ // pending update to Sibling should not be allowed to finish, because it's
1539
+ // part of the async action.
1540
+ await act(() => {
1541
+ root.render(<App showUpdater={false} />);
1542
+ });
1543
+ assertLog(['A']);
1544
+ expect(root).toMatchRenderedOutput(<span>A</span>);
1545
+
1546
+ // Finish the async action. Notice the intermediate B state was never
1547
+ // shown, because it was batched with the update that came later in the
1548
+ // same action.
1549
+ await act(() => resolveText('Wait before updating to C'));
1550
+ assertLog(['Async action ended', 'C']);
1551
+ expect(root).toMatchRenderedOutput(<span>C</span>);
1552
+
1553
+ // Check that subsequent updates are unaffected.
1554
+ await act(() => setText('D'));
1555
+ assertLog(['D']);
1556
+ expect(root).toMatchRenderedOutput(<span>D</span>);
1557
+ },
1558
+ );
1559
+
1560
+ // @gate enableAsyncActions
1561
+ test(
1562
+ 'updates in an async action are entangled even if useTransition hook ' +
1563
+ 'is unmounted before it finishes (root update)',
1564
+ async () => {
1565
+ let startTransition;
1566
+ function Updater() {
1567
+ const [isPending, _start] = useTransition();
1568
+ startTransition = _start;
1569
+ return (
1570
+ <span>
1571
+ <Text text={'Pending: ' + isPending} />
1572
+ </span>
1573
+ );
1574
+ }
1575
+
1576
+ let setShowUpdater;
1577
+ function App({text}) {
1578
+ const [showUpdater, _setShowUpdater] = useState(true);
1579
+ setShowUpdater = _setShowUpdater;
1580
+ return (
1581
+ <>
1582
+ {showUpdater ? <Updater /> : null}
1583
+ <span>
1584
+ <Text text={text} />
1585
+ </span>
1586
+ </>
1587
+ );
1588
+ }
1589
+
1590
+ const root = ReactNoop.createRoot();
1591
+ await act(() => {
1592
+ root.render(<App text="A" />);
1593
+ });
1594
+ assertLog(['Pending: false', 'A']);
1595
+ expect(root).toMatchRenderedOutput(
1596
+ <>
1597
+ <span>Pending: false</span>
1598
+ <span>A</span>
1599
+ </>,
1600
+ );
1601
+
1602
+ // Start an async action that has multiple updates with async
1603
+ // operations in between.
1604
+ await act(() => {
1605
+ startTransition(async () => {
1606
+ Scheduler.log('Async action started');
1607
+ startTransition(() => root.render(<App text="B" />));
1608
+
1609
+ await getText('Wait before updating to C');
1610
+
1611
+ Scheduler.log('Async action ended');
1612
+ startTransition(() => root.render(<App text="C" />));
1613
+ });
1614
+ });
1615
+ assertLog(['Async action started', 'Pending: true']);
1616
+ expect(root).toMatchRenderedOutput(
1617
+ <>
1618
+ <span>Pending: true</span>
1619
+ <span>A</span>
1620
+ </>,
1621
+ );
1622
+
1623
+ // Delete the component that contains the useTransition hook. This
1624
+ // component no longer blocks the transition from completing. But the
1625
+ // pending update to Sibling should not be allowed to finish, because it's
1626
+ // part of the async action.
1627
+ await act(() => setShowUpdater(false));
1628
+ assertLog(['A']);
1629
+ expect(root).toMatchRenderedOutput(<span>A</span>);
1630
+
1631
+ // Finish the async action. Notice the intermediate B state was never
1632
+ // shown, because it was batched with the update that came later in the
1633
+ // same action.
1634
+ await act(() => resolveText('Wait before updating to C'));
1635
+ assertLog(['Async action ended', 'C']);
1636
+ expect(root).toMatchRenderedOutput(<span>C</span>);
1637
+
1638
+ // Check that subsequent updates are unaffected.
1639
+ await act(() => root.render(<App text="D" />));
1640
+ assertLog(['D']);
1641
+ expect(root).toMatchRenderedOutput(<span>D</span>);
1642
+ },
1643
+ );
1644
});