@@ -1461,4 +1461,184 @@ describe('ReactAsyncActions', () => {
14611461 expect ( root ) . toMatchRenderedOutput ( < span > C</ span > ) ;
14621462 } ,
14631463 ) ;
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+ ) ;
14641644} ) ;
0 commit comments