Repository: facebook/react
Pull request: [react-dom] Fire onReset when automatically resetting forms

Fixes https://github.com/facebook/react/issues/33630

During the mutation phase, we disabled the event emitter. This caused `onReset` to be dropped when we [automatically reset the form when a Form Action finishes](https://github.com/facebook/react/pull/28804). Controlled inputs would not have been reset if they're reset during `onReset`. 

This could be implemented by scheduling the state updates from the Form Action instead. Arguably a footgun since you have to consider both now.

People could already work around by listening to the native `reset` event.

Changed files:
filename: fixtures/dom/src/components/Header.js
patch: @@ -88,6 +88,7 @@ class Header extends React.Component {
                 <option value="/mouse-events">Mouse Events</option>
                 <option value="/selection-events">Selection Events</option>
                 <option value="/suspense">Suspense</option>
+                <option value="/form-actions">Form Actions</option>
                 <option value="/form-state">Form State</option>
                 <option value="/fragment-refs">Fragment Refs</option>
               </select>

filename: fixtures/dom/src/components/fixtures/form-actions/index.js
patch: @@ -0,0 +1,113 @@
+const React = window.React;
+
+const {useState} = React;
+
+async function defer(timeoutMS) {
+  return new Promise(resolve => {
+    setTimeout(resolve, timeoutMS);
+  });
+}
+
+export default function FormActions() {
+  const [textValue, setTextValue] = useState('0');
+  const [radioValue, setRadioValue] = useState('two');
+  const [checkboxValue, setCheckboxValue] = useState([false, true, true]);
+  const [selectValue, setSelectValue] = useState('three');
+
+  return (
+    <form
+      action={async () => {
+        await defer(500);
+      }}
+      onReset={() => {
+        setTextValue('0');
+        setRadioValue('two');
+        setCheckboxValue([false, true, true]);
+        setSelectValue('three');
+      }}>
+      <div style={{display: 'flex'}}>
+        <fieldset style={{flexBasis: 0}}>
+          <legend>type="text"</legend>
+          <input
+            type="text"
+            name="text"
+            value={textValue}
+            onChange={event => setTextValue(event.currentTarget.value)}
+          />
+        </fieldset>
+        <fieldset style={{flexBasis: 0}}>
+          <legend>type="radio"</legend>
+          <input
+            type="radio"
+            name="radio"
+            value="one"
+            checked={radioValue === 'one'}
+            onChange={() => setRadioValue('one')}
+          />
+          <input
+            type="radio"
+            name="radio"
+            value="two"
+            checked={radioValue === 'two'}
+            onChange={() => setRadioValue('two')}
+          />
+          <input
+            type="radio"
+            name="radio"
+            value="three"
+            checked={radioValue === 'three'}
+            onChange={() => setRadioValue('three')}
+          />
+        </fieldset>
+        <fieldset style={{flexBasis: 0}}>
+          <legend>type="checkbox"</legend>
+          <input
+            type="checkbox"
+            name="checkbox"
+            value="one"
+            checked={checkboxValue[0]}
+            onChange={event => {
+              const checked = event.currentTarget.checked;
+              setCheckboxValue(pending => [checked, pending[1], pending[2]]);
+            }}
+          />
+          <input
+            type="checkbox"
+            name="checkbox"
+            value="two"
+            checked={checkboxValue[1]}
+            onChange={event => {
+              const checked = event.currentTarget.checked;
+              setCheckboxValue(pending => [pending[0], checked, pending[2]]);
+            }}
+          />
+          <input
+            type="checkbox"
+            name="checkbox"
+            value="three"
+            checked={checkboxValue[2]}
+            onChange={event => {
+              const checked = event.currentTarget.checked;
+              setCheckboxValue(pending => [pending[0], pending[1], checked]);
+            }}
+          />
+        </fieldset>
+        <fieldset style={{flexBasis: 0}}>
+          <legend>select</legend>
+          <select
+            name="select"
+            value={selectValue}
+            onChange={event => setSelectValue(event.currentTarget.value)}>
+            <option value="one">one</option>
+            <option value="two">two</option>
+            <option value="three">three</option>
+          </select>
+        </fieldset>
+      </div>
+      <div>
+        <input type="reset" />
+        <input type="submit" />
+      </div>
+    </form>
+  );
+}

filename: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
patch: @@ -6562,5 +6562,7 @@ export const HostTransitionContext: ReactContext<TransitionStatus> = {
 
 export type FormInstance = HTMLFormElement;
 export function resetFormInstance(form: FormInstance): void {
+  ReactBrowserEventEmitterSetEnabled(true);
   form.reset();
+  ReactBrowserEventEmitterSetEnabled(false);
 }

filename: packages/react-dom/src/__tests__/ReactDOMForm-test.js
patch: @@ -1596,6 +1596,57 @@ describe('ReactDOMForm', () => {
     expect(divRef.current.textContent).toEqual('Current username: acdlite');
   });
 
+  it('should fire onReset on automatic form reset', async () => {
+    const formRef = React.createRef();
+    const inputRef = React.createRef();
+
+    let setValue;
+    const defaultValue = 0;
+    function App({promiseForUsername}) {
+      const [value, _setValue] = useState(defaultValue);
+      setValue = _setValue;
+
+      return (
+        <form
+          ref={formRef}
+          action={async formData => {
+            Scheduler.log(`Async action started`);
+            await getText('Wait');
+          }}
+          onReset={() => {
+            setValue(defaultValue);
+          }}>
+          <input
+            ref={inputRef}
+            text="text"
+            name="amount"
+            value={value}
+            onChange={event => setValue(event.currentTarget.value)}
+          />
+        </form>
+      );
+    }
+
+    const root = ReactDOMClient.createRoot(container);
+    await act(() => root.render(<App />));
+
+    // Dirty the controlled input
+    await act(() => setValue('3'));
+    expect(inputRef.current.value).toEqual('3');
+
+    // Submit the form. This will trigger an async action.
+    await submit(formRef.current);
+    assertLog(['Async action started']);
+
+    // We haven't reset yet.
+    expect(inputRef.current.value).toEqual('3');
+
+    // Action completes. onReset has been fired and values reset manually.
+    await act(() => resolveText('Wait'));
+    assertLog([]);
+    expect(inputRef.current.value).toEqual('0');
+  });
+
   it('requestFormReset schedules a form reset after transition completes', async () => {
     // This is the same as the previous test, except the form is updated with
     // a userspace action instead of a built-in form action.