@samitouri / QOS-React-1 / commits / dcab44d757

[react-dom] Fire onReset when automatically resetting forms (#35176)

Sebastian "Sebbie" Silbermann committed Feb 2, 2026 at 21:17 UTC dcab44d757dee6fa8d40db56715732e891a3b729
4 files changed +167
fixtures/dom/src/components/Header.js
+1
@@ -88,6 +88,7 @@ class Header extends React.Component {
88 <option value="/mouse-events">Mouse Events</option>
89 <option value="/selection-events">Selection Events</option>
90 <option value="/suspense">Suspense</option>
91 + <option value="/form-actions">Form Actions</option>
92 <option value="/form-state">Form State</option>
93 <option value="/fragment-refs">Fragment Refs</option>
94 </select>
fixtures/dom/src/components/fixtures/form-actions/index.js new
+113
@@ -0,0 +1,113 @@
1 +const React = window.React;
2 +
3 +const {useState} = React;
4 +
5 +async function defer(timeoutMS) {
6 + return new Promise(resolve => {
7 + setTimeout(resolve, timeoutMS);
8 + });
9 +}
10 +
11 +export default function FormActions() {
12 + const [textValue, setTextValue] = useState('0');
13 + const [radioValue, setRadioValue] = useState('two');
14 + const [checkboxValue, setCheckboxValue] = useState([false, true, true]);
15 + const [selectValue, setSelectValue] = useState('three');
16 +
17 + return (
18 + <form
19 + action={async () => {
20 + await defer(500);
21 + }}
22 + onReset={() => {
23 + setTextValue('0');
24 + setRadioValue('two');
25 + setCheckboxValue([false, true, true]);
26 + setSelectValue('three');
27 + }}>
28 + <div style={{display: 'flex'}}>
29 + <fieldset style={{flexBasis: 0}}>
30 + <legend>type="text"</legend>
31 + <input
32 + type="text"
33 + name="text"
34 + value={textValue}
35 + onChange={event => setTextValue(event.currentTarget.value)}
36 + />
37 + </fieldset>
38 + <fieldset style={{flexBasis: 0}}>
39 + <legend>type="radio"</legend>
40 + <input
41 + type="radio"
42 + name="radio"
43 + value="one"
44 + checked={radioValue === 'one'}
45 + onChange={() => setRadioValue('one')}
46 + />
47 + <input
48 + type="radio"
49 + name="radio"
50 + value="two"
51 + checked={radioValue === 'two'}
52 + onChange={() => setRadioValue('two')}
53 + />
54 + <input
55 + type="radio"
56 + name="radio"
57 + value="three"
58 + checked={radioValue === 'three'}
59 + onChange={() => setRadioValue('three')}
60 + />
61 + </fieldset>
62 + <fieldset style={{flexBasis: 0}}>
63 + <legend>type="checkbox"</legend>
64 + <input
65 + type="checkbox"
66 + name="checkbox"
67 + value="one"
68 + checked={checkboxValue[0]}
69 + onChange={event => {
70 + const checked = event.currentTarget.checked;
71 + setCheckboxValue(pending => [checked, pending[1], pending[2]]);
72 + }}
73 + />
74 + <input
75 + type="checkbox"
76 + name="checkbox"
77 + value="two"
78 + checked={checkboxValue[1]}
79 + onChange={event => {
80 + const checked = event.currentTarget.checked;
81 + setCheckboxValue(pending => [pending[0], checked, pending[2]]);
82 + }}
83 + />
84 + <input
85 + type="checkbox"
86 + name="checkbox"
87 + value="three"
88 + checked={checkboxValue[2]}
89 + onChange={event => {
90 + const checked = event.currentTarget.checked;
91 + setCheckboxValue(pending => [pending[0], pending[1], checked]);
92 + }}
93 + />
94 + </fieldset>
95 + <fieldset style={{flexBasis: 0}}>
96 + <legend>select</legend>
97 + <select
98 + name="select"
99 + value={selectValue}
100 + onChange={event => setSelectValue(event.currentTarget.value)}>
101 + <option value="one">one</option>
102 + <option value="two">two</option>
103 + <option value="three">three</option>
104 + </select>
105 + </fieldset>
106 + </div>
107 + <div>
108 + <input type="reset" />
109 + <input type="submit" />
110 + </div>
111 + </form>
112 + );
113 +}
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+2
@@ -6562,5 +6562,7 @@ export const HostTransitionContext: ReactContext<TransitionStatus> = {
6562
6563 export type FormInstance = HTMLFormElement;
6564 export function resetFormInstance(form: FormInstance): void {
6565 + ReactBrowserEventEmitterSetEnabled(true);
6566 form.reset();
6567 + ReactBrowserEventEmitterSetEnabled(false);
6568 }
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+51
@@ -1596,6 +1596,57 @@ describe('ReactDOMForm', () => {
1596 expect(divRef.current.textContent).toEqual('Current username: acdlite');
1597 });
1598
1599 + it('should fire onReset on automatic form reset', async () => {
1600 + const formRef = React.createRef();
1601 + const inputRef = React.createRef();
1602 +
1603 + let setValue;
1604 + const defaultValue = 0;
1605 + function App({promiseForUsername}) {
1606 + const [value, _setValue] = useState(defaultValue);
1607 + setValue = _setValue;
1608 +
1609 + return (
1610 + <form
1611 + ref={formRef}
1612 + action={async formData => {
1613 + Scheduler.log(`Async action started`);
1614 + await getText('Wait');
1615 + }}
1616 + onReset={() => {
1617 + setValue(defaultValue);
1618 + }}>
1619 + <input
1620 + ref={inputRef}
1621 + text="text"
1622 + name="amount"
1623 + value={value}
1624 + onChange={event => setValue(event.currentTarget.value)}
1625 + />
1626 + </form>
1627 + );
1628 + }
1629 +
1630 + const root = ReactDOMClient.createRoot(container);
1631 + await act(() => root.render(<App />));
1632 +
1633 + // Dirty the controlled input
1634 + await act(() => setValue('3'));
1635 + expect(inputRef.current.value).toEqual('3');
1636 +
1637 + // Submit the form. This will trigger an async action.
1638 + await submit(formRef.current);
1639 + assertLog(['Async action started']);
1640 +
1641 + // We haven't reset yet.
1642 + expect(inputRef.current.value).toEqual('3');
1643 +
1644 + // Action completes. onReset has been fired and values reset manually.
1645 + await act(() => resolveText('Wait'));
1646 + assertLog([]);
1647 + expect(inputRef.current.value).toEqual('0');
1648 + });
1649 +
1650 it('requestFormReset schedules a form reset after transition completes', async () => {
1651 // This is the same as the previous test, except the form is updated with
1652 // a userspace action instead of a built-in form action.