Repository: facebook/react
Pull request: Use `FormData` `submitter` parameter

## Summary

Fixes #29018
Fixes #34944
Closes https://github.com/facebook/react/pull/34990
Closes https://github.com/facebook/react/pull/35354

Rather than continuing to maintain/fix an incomplete polyfill (e.g. #28056, #34990), just pass the `submitter` to the `FormData` constructor, since it is now [widely available](https://caniuse.com/?search=FormData.submitter). This ensures that the form data is populated consistently with a native form submission, and lets us remove a whole lot of code. This is a followup to [this discussion](https://github.com/facebook/react/pull/26674/files/5a7629ddb4f22072e37ba1ae581977ce03bdf288#r1178495317) over two years ago.

Elaborating on that, if we're concerned about browsers that don't support it, it's important to note that:

- Most of them also [don't support `SubmitEvent.submitter`](https://caniuse.com/?search=SubmitEvent.submitter), which means the existing approach won't work for them either
- The `FormData` `submitter` [can easily be polyfilled](https://www.npmjs.com/package/formdata-submitter-polyfill) (as [can](https://www.npmjs.com/package/event-submitter-polyfill) `SubmitEvent.submitter`) for developers that care about named submit buttons working correctly in old browsers

Older browsers (Safari on old iOS, Samsung TVs), can polyfill `FormData` the way React did:
```js
const OriginalFormData = globalThis.FormData

class FormDataWithSubmitterPolyfill extends OriginalFormData {
  constructor(form, submitter) {
    console.log(form, submitter)
    if (submitter != null) {
      const temp = submitter.ownerDocument.createElement('input');
      temp.name = submitter.name;
      temp.value = submitter.value;
      if (form.id) {
        temp.setAttribute('form', form.id);
      }
      submitter.parentNode.insertBefore(temp, submitter);
      super(form)
      temp.parentNode.removeChild(temp)
    } else {
      super(form)
    }

  }
}
globalThis.FormData = FormDataWithSubmitterPolyfill
```
Note that the above doesn't apply to native submissions but neither did React's behavior. It only applied to React's Form Actions.

## How did you test this change?

Updated an existing test to more fully cover `submitter` serialization, and ran `yarn test` and `yarn test --prod` successfully.

<!--
  Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface.
  How exactly did you verify that your PR solves the issue you wanted to solve?
  If you leave this empty, your PR will very likely be closed.
-->


Changed files:
filename: packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js
patch: @@ -45,30 +45,6 @@ function coerceFormActionProp(
   }
 }
 
-function createFormDataWithSubmitter(
-  form: HTMLFormElement,
-  submitter: HTMLInputElement | HTMLButtonElement,
-) {
-  // The submitter's value should be included in the FormData.
-  // It should be in the document order in the form.
-  // Since the FormData constructor invokes the formdata event it also
-  // needs to be available before that happens so after construction it's too
-  // late. We use a temporary fake node for the duration of this event.
-  // TODO: FormData takes a second argument that it's the submitter but this
-  // is fairly new so not all browsers support it yet. Switch to that technique
-  // when available.
-  const temp = submitter.ownerDocument.createElement('input');
-  temp.name = submitter.name;
-  temp.value = submitter.value;
-  if (form.id) {
-    temp.setAttribute('form', form.id);
-  }
-  (submitter.parentNode: any).insertBefore(temp, submitter);
-  const formData = new FormData(form);
-  (temp.parentNode: any).removeChild(temp);
-  return formData;
-}
-
 /**
  * This plugin invokes action functions on forms, inputs and buttons if
  * the form doesn't prevent default.
@@ -129,9 +105,7 @@ function extractEvents(
       if (didCurrentEventScheduleTransition()) {
         // We're going to set the pending form status, but because the submission
         // was prevented, we should not fire the action function.
-        const formData = submitter
-          ? createFormDataWithSubmitter(form, submitter)
-          : new FormData(form);
+        const formData = new FormData(form, submitter);
         const pendingState: FormStatus = {
           pending: true,
           data: formData,
@@ -160,9 +134,7 @@ function extractEvents(
       event.preventDefault();
 
       // Dispatch the action and set a pending form status.
-      const formData = submitter
-        ? createFormDataWithSubmitter(form, submitter)
-        : new FormData(form);
+      const formData = new FormData(form, submitter);
       const pendingState: FormStatus = {
         pending: true,
         data: formData,

filename: packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
patch: @@ -14,4 +14,4 @@ export const completeBoundaryWithStyles =
 export const completeSegment =
   '$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};';
 export const formReplaying =
-  'addEventListener("submit",function(a){if(!a.defaultPrevented){var c=a.target,d=a.submitter,e=c.action,b=d;if(d){var f=d.getAttribute("formAction");null!=f&&(e=f,b=null)}"javascript:throw new Error(\'React form unexpectedly submitted.\')"===e&&(a.preventDefault(),b?(a=document.createElement("input"),a.name=b.name,a.value=b.value,b.parentNode.insertBefore(a,b),b=new FormData(c),a.parentNode.removeChild(a)):b=new FormData(c),a=c.ownerDocument||c,(a.$$reactFormReplay=a.$$reactFormReplay||[]).push(c,d,b))}});';
+  'addEventListener("submit",function(a){if(!a.defaultPrevented){var b=a.target,d=a.submitter,c=b.action,e=d;if(d){var f=d.getAttribute("formAction");null!=f&&(c=f,e=null)}"javascript:throw new Error(\'React form unexpectedly submitted.\')"===c&&(a.preventDefault(),a=new FormData(b,e),c=b.ownerDocument||b,(c.$$reactFormReplay=c.$$reactFormReplay||[]).push(b,d,a))}});';

filename: packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
patch: @@ -634,25 +634,7 @@ export function listenToFormSubmissionsForReplaying() {
     event.preventDefault();
 
     // Take a snapshot of the FormData at the time of the event.
-    let formData;
-    if (formDataSubmitter) {
-      // The submitter's value should be included in the FormData.
-      // It should be in the document order in the form.
-      // Since the FormData constructor invokes the formdata event it also
-      // needs to be available before that happens so after construction it's too
-      // late. We use a temporary fake node for the duration of this event.
-      // TODO: FormData takes a second argument that it's the submitter but this
-      // is fairly new so not all browsers support it yet. Switch to that technique
-      // when available.
-      const temp = document.createElement('input');
-      temp.name = formDataSubmitter.name;
-      temp.value = formDataSubmitter.value;
-      formDataSubmitter.parentNode.insertBefore(temp, formDataSubmitter);
-      formData = new FormData(form);
-      temp.parentNode.removeChild(temp);
-    } else {
-      formData = new FormData(form);
-    }
+    const formData = new FormData(form, formDataSubmitter);
 
     // Queue for replaying later. This field could potentially be shared with multiple
     // Reacts on the same page since each one will preventDefault for the next one.

filename: packages/react-dom/src/__tests__/ReactDOMForm-test.js
patch: @@ -14,8 +14,8 @@ global.IS_REACT_ACT_ENVIRONMENT = true;
 // Our current version of JSDOM doesn't implement the event dispatching
 // so we polyfill it.
 const NativeFormData = global.FormData;
-const FormDataPolyfill = function FormData(form) {
-  const formData = new NativeFormData(form);
+const FormDataPolyfill = function FormData(form, submitter) {
+  const formData = new NativeFormData(form, submitter);
   const formDataEvent = new Event('formdata', {
     bubbles: true,
     cancelable: false,
@@ -489,11 +489,16 @@ describe('ReactDOMForm', () => {
     const inputRef = React.createRef();
     const buttonRef = React.createRef();
     const outsideButtonRef = React.createRef();
+    const imageButtonRef = React.createRef();
     let button;
+    let buttonX;
+    let buttonY;
     let title;
 
     function action(formData) {
       button = formData.get('button');
+      buttonX = formData.get('button.x');
+      buttonY = formData.get('button.y');
       title = formData.get('title');
     }
 
@@ -508,6 +513,12 @@ describe('ReactDOMForm', () => {
             <button name="button" value="edit" ref={buttonRef}>
               Edit
             </button>
+            <input
+              type="image"
+              name="button"
+              href="/some/image.png"
+              ref={imageButtonRef}
+            />
           </form>
           <form id="form" action={action}>
             <input type="text" name="title" defaultValue="hello" />
@@ -546,9 +557,12 @@ describe('ReactDOMForm', () => {
     expect(button).toBe('outside');
     expect(title).toBe('hello');
 
-    // Ensure that the type field got correctly restored
-    expect(inputRef.current.getAttribute('type')).toBe('submit');
-    expect(buttonRef.current.getAttribute('type')).toBe(null);
+    await submit(imageButtonRef.current);
+
+    expect(button).toBe(null);
+    expect(buttonX).toBe('0');
+    expect(buttonY).toBe('0');
+    expect(title).toBe('hello');
   });
 
   it('excludes the submitter name when the submitter is a function action', async () => {

filename: packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
patch: @@ -121,17 +121,7 @@ describe('ReactFlightDOMForm', () => {
       const method = (submitter && submitter.formMethod) || form.method;
       const encType = (submitter && submitter.formEnctype) || form.enctype;
       if (method === 'post' && encType === 'multipart/form-data') {
-        let formData;
-        if (submitter) {
-          const temp = document.createElement('input');
-          temp.name = submitter.name;
-          temp.value = submitter.value;
-          submitter.parentNode.insertBefore(temp, submitter);
-          formData = new FormData(form);
-          temp.parentNode.removeChild(temp);
-        } else {
-          formData = new FormData(form);
-        }
+        const formData = new FormData(form, submitter);
         return POST(formData);
       }
       throw new Error('Navigate to: ' + action);