@@ -43,6 +43,7 @@ describe('ReactDOMForm', () => {
43
43
let textCache ;
44
44
let useFormStatus ;
45
45
let useActionState ;
46
+ let requestFormReset ;
46
47
47
48
beforeEach ( ( ) => {
48
49
jest . resetModules ( ) ;
@@ -58,6 +59,7 @@ describe('ReactDOMForm', () => {
58
59
startTransition = React . startTransition ;
59
60
use = React . use ;
60
61
useFormStatus = ReactDOM . useFormStatus ;
62
+ requestFormReset = ReactDOM . requestFormReset ;
61
63
container = document . createElement ( 'div' ) ;
62
64
document . body . appendChild ( container ) ;
63
65
@@ -1414,4 +1416,351 @@ describe('ReactDOMForm', () => {
1414
1416
expect ( inputRef . current . value ) . toBe ( 'acdlite' ) ;
1415
1417
expect ( divRef . current . textContent ) . toEqual ( 'Current username: acdlite' ) ;
1416
1418
} ) ;
1419
+
1420
+ test ( 'requestFormReset schedules a form reset after transition completes' , async ( ) => {
1421
+ // This is the same as the previous test, except the form is updated with
1422
+ // a userspace action instead of a built-in form action.
1423
+
1424
+ const formRef = React . createRef ( ) ;
1425
+ const inputRef = React . createRef ( ) ;
1426
+ const divRef = React . createRef ( ) ;
1427
+
1428
+ function App ( { promiseForUsername} ) {
1429
+ // Make this suspensey to simulate RSC streaming.
1430
+ const username = use ( promiseForUsername ) ;
1431
+
1432
+ return (
1433
+ < form ref = { formRef } >
1434
+ < input
1435
+ ref = { inputRef }
1436
+ text = "text"
1437
+ name = "username"
1438
+ defaultValue = { username }
1439
+ />
1440
+ < div ref = { divRef } >
1441
+ < Text text = { 'Current username: ' + username } />
1442
+ </ div >
1443
+ </ form >
1444
+ ) ;
1445
+ }
1446
+
1447
+ // Initial render
1448
+ const root = ReactDOMClient . createRoot ( container ) ;
1449
+ const promiseForInitialUsername = getText ( '(empty)' ) ;
1450
+ await resolveText ( '(empty)' ) ;
1451
+ await act ( ( ) =>
1452
+ root . render ( < App promiseForUsername = { promiseForInitialUsername } /> ) ,
1453
+ ) ;
1454
+ assertLog ( [ 'Current username: (empty)' ] ) ;
1455
+ expect ( divRef . current . textContent ) . toEqual ( 'Current username: (empty)' ) ;
1456
+
1457
+ // Dirty the uncontrolled input
1458
+ inputRef . current . value = ' AcdLite ' ;
1459
+
1460
+ // This is a userspace action. It does not trigger a real form submission.
1461
+ // The practical use case is implementing a custom action prop using
1462
+ // onSubmit without losing the built-in form resetting behavior.
1463
+ await act ( ( ) => {
1464
+ startTransition ( async ( ) => {
1465
+ const form = formRef . current ;
1466
+ const formData = new FormData ( form ) ;
1467
+ requestFormReset ( form ) ;
1468
+
1469
+ const rawUsername = formData . get ( 'username' ) ;
1470
+ const normalizedUsername = rawUsername . trim ( ) . toLowerCase ( ) ;
1471
+
1472
+ Scheduler . log ( `Async action started` ) ;
1473
+ await getText ( 'Wait' ) ;
1474
+
1475
+ // Update the app with new data. This is analagous to re-rendering
1476
+ // from the root with a new RSC payload.
1477
+ startTransition ( ( ) => {
1478
+ root . render ( < App promiseForUsername = { getText ( normalizedUsername ) } /> ) ;
1479
+ } ) ;
1480
+ } ) ;
1481
+ } ) ;
1482
+ assertLog ( [ 'Async action started' ] ) ;
1483
+ expect ( inputRef . current . value ) . toBe ( ' AcdLite ' ) ;
1484
+
1485
+ // Finish the async action. This will trigger a re-render from the root with
1486
+ // new data from the "server", which suspends.
1487
+ //
1488
+ // The form should not reset yet because we need to update `defaultValue`
1489
+ // first. So we wait for the render to complete.
1490
+ await act ( ( ) => resolveText ( 'Wait' ) ) ;
1491
+ assertLog ( [ ] ) ;
1492
+ // The DOM input is still dirty.
1493
+ expect ( inputRef . current . value ) . toBe ( ' AcdLite ' ) ;
1494
+ // The React tree is suspended.
1495
+ expect ( divRef . current . textContent ) . toEqual ( 'Current username: (empty)' ) ;
1496
+
1497
+ // Unsuspend and finish rendering. Now the form should be reset.
1498
+ await act ( ( ) => resolveText ( 'acdlite' ) ) ;
1499
+ assertLog ( [ 'Current username: acdlite' ] ) ;
1500
+ // The form was reset to the new value from the server.
1501
+ expect ( inputRef . current . value ) . toBe ( 'acdlite' ) ;
1502
+ expect ( divRef . current . textContent ) . toEqual ( 'Current username: acdlite' ) ;
1503
+ } ) ;
1504
+
1505
+ test (
1506
+ 'requestFormReset works with inputs that are not descendants ' +
1507
+ 'of the form element' ,
1508
+ async ( ) => {
1509
+ // This is the same as the previous test, except the input is not a child
1510
+ // of the form; it's linked with <input form="myform" />
1511
+
1512
+ const formRef = React . createRef ( ) ;
1513
+ const inputRef = React . createRef ( ) ;
1514
+ const divRef = React . createRef ( ) ;
1515
+
1516
+ function App ( { promiseForUsername} ) {
1517
+ // Make this suspensey to simulate RSC streaming.
1518
+ const username = use ( promiseForUsername ) ;
1519
+
1520
+ return (
1521
+ < >
1522
+ < form id = "myform" ref = { formRef } />
1523
+ < input
1524
+ form = "myform"
1525
+ ref = { inputRef }
1526
+ text = "text"
1527
+ name = "username"
1528
+ defaultValue = { username }
1529
+ />
1530
+ < div ref = { divRef } >
1531
+ < Text text = { 'Current username: ' + username } />
1532
+ </ div >
1533
+ </ >
1534
+ ) ;
1535
+ }
1536
+
1537
+ // Initial render
1538
+ const root = ReactDOMClient . createRoot ( container ) ;
1539
+ const promiseForInitialUsername = getText ( '(empty)' ) ;
1540
+ await resolveText ( '(empty)' ) ;
1541
+ await act ( ( ) =>
1542
+ root . render ( < App promiseForUsername = { promiseForInitialUsername } /> ) ,
1543
+ ) ;
1544
+ assertLog ( [ 'Current username: (empty)' ] ) ;
1545
+ expect ( divRef . current . textContent ) . toEqual ( 'Current username: (empty)' ) ;
1546
+
1547
+ // Dirty the uncontrolled input
1548
+ inputRef . current . value = ' AcdLite ' ;
1549
+
1550
+ // This is a userspace action. It does not trigger a real form submission.
1551
+ // The practical use case is implementing a custom action prop using
1552
+ // onSubmit without losing the built-in form resetting behavior.
1553
+ await act ( ( ) => {
1554
+ startTransition ( async ( ) => {
1555
+ const form = formRef . current ;
1556
+ const formData = new FormData ( form ) ;
1557
+ requestFormReset ( form ) ;
1558
+
1559
+ const rawUsername = formData . get ( 'username' ) ;
1560
+ const normalizedUsername = rawUsername . trim ( ) . toLowerCase ( ) ;
1561
+
1562
+ Scheduler . log ( `Async action started` ) ;
1563
+ await getText ( 'Wait' ) ;
1564
+
1565
+ // Update the app with new data. This is analagous to re-rendering
1566
+ // from the root with a new RSC payload.
1567
+ startTransition ( ( ) => {
1568
+ root . render (
1569
+ < App promiseForUsername = { getText ( normalizedUsername ) } /> ,
1570
+ ) ;
1571
+ } ) ;
1572
+ } ) ;
1573
+ } ) ;
1574
+ assertLog ( [ 'Async action started' ] ) ;
1575
+ expect ( inputRef . current . value ) . toBe ( ' AcdLite ' ) ;
1576
+
1577
+ // Finish the async action. This will trigger a re-render from the root with
1578
+ // new data from the "server", which suspends.
1579
+ //
1580
+ // The form should not reset yet because we need to update `defaultValue`
1581
+ // first. So we wait for the render to complete.
1582
+ await act ( ( ) => resolveText ( 'Wait' ) ) ;
1583
+ assertLog ( [ ] ) ;
1584
+ // The DOM input is still dirty.
1585
+ expect ( inputRef . current . value ) . toBe ( ' AcdLite ' ) ;
1586
+ // The React tree is suspended.
1587
+ expect ( divRef . current . textContent ) . toEqual ( 'Current username: (empty)' ) ;
1588
+
1589
+ // Unsuspend and finish rendering. Now the form should be reset.
1590
+ await act ( ( ) => resolveText ( 'acdlite' ) ) ;
1591
+ assertLog ( [ 'Current username: acdlite' ] ) ;
1592
+ // The form was reset to the new value from the server.
1593
+ expect ( inputRef . current . value ) . toBe ( 'acdlite' ) ;
1594
+ expect ( divRef . current . textContent ) . toEqual ( 'Current username: acdlite' ) ;
1595
+ } ,
1596
+ ) ;
1597
+
1598
+ test ( 'reset multiple forms in the same transition' , async ( ) => {
1599
+ const formRefA = React . createRef ( ) ;
1600
+ const formRefB = React . createRef ( ) ;
1601
+
1602
+ function App ( { promiseForA, promiseForB} ) {
1603
+ // Make these suspensey to simulate RSC streaming.
1604
+ const a = use ( promiseForA ) ;
1605
+ const b = use ( promiseForB ) ;
1606
+ return (
1607
+ < >
1608
+ < form ref = { formRefA } >
1609
+ < input type = "text" name = "inputName" defaultValue = { a } />
1610
+ </ form >
1611
+ < form ref = { formRefB } >
1612
+ < input type = "text" name = "inputName" defaultValue = { b } />
1613
+ </ form >
1614
+ </ >
1615
+ ) ;
1616
+ }
1617
+
1618
+ const root = ReactDOMClient . createRoot ( container ) ;
1619
+ const initialPromiseForA = getText ( 'A1' ) ;
1620
+ const initialPromiseForB = getText ( 'B1' ) ;
1621
+ await resolveText ( 'A1' ) ;
1622
+ await resolveText ( 'B1' ) ;
1623
+ await act ( ( ) =>
1624
+ root . render (
1625
+ < App
1626
+ promiseForA = { initialPromiseForA }
1627
+ promiseForB = { initialPromiseForB }
1628
+ /> ,
1629
+ ) ,
1630
+ ) ;
1631
+
1632
+ // Dirty the uncontrolled inputs
1633
+ formRefA . current . elements . inputName . value = ' A2 ' ;
1634
+ formRefB . current . elements . inputName . value = ' B2 ' ;
1635
+
1636
+ // Trigger an async action that updates and reset both forms.
1637
+ await act ( ( ) => {
1638
+ startTransition ( async ( ) => {
1639
+ const currentA = formRefA . current . elements . inputName . value ;
1640
+ const currentB = formRefB . current . elements . inputName . value ;
1641
+
1642
+ requestFormReset ( formRefA . current ) ;
1643
+ requestFormReset ( formRefB . current ) ;
1644
+
1645
+ Scheduler . log ( 'Async action started' ) ;
1646
+ await getText ( 'Wait' ) ;
1647
+
1648
+ // Pretend the server did something with the data.
1649
+ const normalizedA = currentA . trim ( ) ;
1650
+ const normalizedB = currentB . trim ( ) ;
1651
+
1652
+ // Update the app with new data. This is analagous to re-rendering
1653
+ // from the root with a new RSC payload.
1654
+ startTransition ( ( ) => {
1655
+ root . render (
1656
+ < App
1657
+ promiseForA = { getText ( normalizedA ) }
1658
+ promiseForB = { getText ( normalizedB ) }
1659
+ /> ,
1660
+ ) ;
1661
+ } ) ;
1662
+ } ) ;
1663
+ } ) ;
1664
+ assertLog ( [ 'Async action started' ] ) ;
1665
+
1666
+ // Finish the async action. This will trigger a re-render from the root with
1667
+ // new data from the "server", which suspends.
1668
+ //
1669
+ // The forms should not reset yet because we need to update `defaultValue`
1670
+ // first. So we wait for the render to complete.
1671
+ await act ( ( ) => resolveText ( 'Wait' ) ) ;
1672
+
1673
+ // The DOM inputs are still dirty.
1674
+ expect ( formRefA . current . elements . inputName . value ) . toBe ( ' A2 ' ) ;
1675
+ expect ( formRefB . current . elements . inputName . value ) . toBe ( ' B2 ' ) ;
1676
+
1677
+ // Unsuspend and finish rendering. Now the forms should be reset.
1678
+ await act ( ( ) => {
1679
+ resolveText ( 'A2' ) ;
1680
+ resolveText ( 'B2' ) ;
1681
+ } ) ;
1682
+ // The forms were reset to the new value from the server.
1683
+ expect ( formRefA . current . elements . inputName . value ) . toBe ( 'A2' ) ;
1684
+ expect ( formRefB . current . elements . inputName . value ) . toBe ( 'B2' ) ;
1685
+ } ) ;
1686
+
1687
+ test ( 'requestFormReset throws if the form is not managed by React' , async ( ) => {
1688
+ container . innerHTML = `
1689
+ <form id="myform">
1690
+ <input id="input" type="text" name="greeting" />
1691
+ </form>
1692
+ ` ;
1693
+
1694
+ const form = document . getElementById ( 'myform' ) ;
1695
+ const input = document . getElementById ( 'input' ) ;
1696
+
1697
+ input . value = 'Hi!!!!!!!!!!!!!' ;
1698
+
1699
+ expect ( ( ) => requestFormReset ( form ) ) . toThrow ( 'Invalid form element.' ) ;
1700
+ // The form was not reset.
1701
+ expect ( input . value ) . toBe ( 'Hi!!!!!!!!!!!!!' ) ;
1702
+
1703
+ // Just confirming a regular form reset works fine.
1704
+ form . reset ( ) ;
1705
+ expect ( input . value ) . toBe ( '' ) ;
1706
+ } ) ;
1707
+
1708
+ test ( 'requestFormReset throws on a non-form DOM element' , async ( ) => {
1709
+ const root = ReactDOMClient . createRoot ( container ) ;
1710
+ const ref = React . createRef ( ) ;
1711
+ await act ( ( ) => root . render ( < div ref = { ref } > Hi</ div > ) ) ;
1712
+ const div = ref . current ;
1713
+ expect ( div . textContent ) . toBe ( 'Hi' ) ;
1714
+
1715
+ expect ( ( ) => requestFormReset ( div ) ) . toThrow ( 'Invalid form element.' ) ;
1716
+ } ) ;
1717
+
1718
+ test ( 'warns if requestFormReset is called outside of a transition' , async ( ) => {
1719
+ const formRef = React . createRef ( ) ;
1720
+ const inputRef = React . createRef ( ) ;
1721
+
1722
+ function App ( ) {
1723
+ return (
1724
+ < form ref = { formRef } >
1725
+ < input ref = { inputRef } type = "text" defaultValue = "Initial" />
1726
+ </ form >
1727
+ ) ;
1728
+ }
1729
+
1730
+ const root = ReactDOMClient . createRoot ( container ) ;
1731
+ await act ( ( ) => root . render ( < App /> ) ) ;
1732
+
1733
+ // Dirty the uncontrolled input
1734
+ inputRef . current . value = ' Updated ' ;
1735
+
1736
+ // Trigger an async action that updates and reset both forms.
1737
+ await act ( ( ) => {
1738
+ startTransition ( async ( ) => {
1739
+ Scheduler . log ( 'Action started' ) ;
1740
+ await getText ( 'Wait 1' ) ;
1741
+ Scheduler . log ( 'Request form reset' ) ;
1742
+
1743
+ // This happens after an `await`, and is not wrapped in startTransition,
1744
+ // so it will be scheduled synchronously instead of with the transition.
1745
+ // This is almost certainly a mistake, so we log a warning in dev.
1746
+ requestFormReset ( formRef . current ) ;
1747
+
1748
+ await getText ( 'Wait 2' ) ;
1749
+ Scheduler . log ( 'Action finished' ) ;
1750
+ } ) ;
1751
+ } ) ;
1752
+ assertLog ( [ 'Action started' ] ) ;
1753
+ expect ( inputRef . current . value ) . toBe ( ' Updated ' ) ;
1754
+
1755
+ // This triggers a synchronous requestFormReset, and a warning
1756
+ await expect ( async ( ) => {
1757
+ await act ( ( ) => resolveText ( 'Wait 1' ) ) ;
1758
+ } ) . toErrorDev ( [ 'requestFormReset was called outside a transition' ] , {
1759
+ withoutStack : true ,
1760
+ } ) ;
1761
+ assertLog ( [ 'Request form reset' ] ) ;
1762
+
1763
+ // The form was reset even though the action didn't finish.
1764
+ expect ( inputRef . current . value ) . toBe ( 'Initial' ) ;
1765
+ } ) ;
1417
1766
} ) ;
0 commit comments