Use `FormData` `submitter` parameter (#29028)
Jon Jensen committed
Dec 18, 2025 at 03:34 UTC
65eec428c40d542d4d5a9c1af5c3f406aecf3440
5 files changed
+24
-66
packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js
+2
-30
@@ -45,30 +45,6 @@ function coerceFormActionProp(
45
}
46
}
47
48
-function createFormDataWithSubmitter(
49
- form: HTMLFormElement,
50
- submitter: HTMLInputElement | HTMLButtonElement,
51
-) {
52
- // The submitter's value should be included in the FormData.
53
- // It should be in the document order in the form.
54
- // Since the FormData constructor invokes the formdata event it also
55
- // needs to be available before that happens so after construction it's too
56
- // late. We use a temporary fake node for the duration of this event.
57
- // TODO: FormData takes a second argument that it's the submitter but this
58
- // is fairly new so not all browsers support it yet. Switch to that technique
59
- // when available.
60
- const temp = submitter.ownerDocument.createElement('input');
61
- temp.name = submitter.name;
62
- temp.value = submitter.value;
63
- if (form.id) {
64
- temp.setAttribute('form', form.id);
65
- }
66
- (submitter.parentNode: any).insertBefore(temp, submitter);
67
- const formData = new FormData(form);
68
- (temp.parentNode: any).removeChild(temp);
69
- return formData;
70
-}
71
-
48
/**
49
* This plugin invokes action functions on forms, inputs and buttons if
50
* the form doesn't prevent default.
@@ -129,9 +105,7 @@ function extractEvents(
105
if (didCurrentEventScheduleTransition()) {
106
// We're going to set the pending form status, but because the submission
107
// was prevented, we should not fire the action function.
132
- const formData = submitter
133
- ? createFormDataWithSubmitter(form, submitter)
134
- : new FormData(form);
108
+ const formData = new FormData(form, submitter);
109
const pendingState: FormStatus = {
110
pending: true,
111
data: formData,
@@ -160,9 +134,7 @@ function extractEvents(
134
event.preventDefault();
135
136
// Dispatch the action and set a pending form status.
163
- const formData = submitter
164
- ? createFormDataWithSubmitter(form, submitter)
165
- : new FormData(form);
137
+ const formData = new FormData(form, submitter);
138
const pendingState: FormStatus = {
139
pending: true,
140
data: formData,
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1
-1
@@ -14,4 +14,4 @@ export const completeBoundaryWithStyles =
14
export const completeSegment =
15
'$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)};';
16
export const formReplaying =
17
- '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))}});';
17
+ '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))}});';
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+1
-19
@@ -634,25 +634,7 @@ export function listenToFormSubmissionsForReplaying() {
634
event.preventDefault();
635
636
// Take a snapshot of the FormData at the time of the event.
637
- let formData;
638
- if (formDataSubmitter) {
639
- // The submitter's value should be included in the FormData.
640
- // It should be in the document order in the form.
641
- // Since the FormData constructor invokes the formdata event it also
642
- // needs to be available before that happens so after construction it's too
643
- // late. We use a temporary fake node for the duration of this event.
644
- // TODO: FormData takes a second argument that it's the submitter but this
645
- // is fairly new so not all browsers support it yet. Switch to that technique
646
- // when available.
647
- const temp = document.createElement('input');
648
- temp.name = formDataSubmitter.name;
649
- temp.value = formDataSubmitter.value;
650
- formDataSubmitter.parentNode.insertBefore(temp, formDataSubmitter);
651
- formData = new FormData(form);
652
- temp.parentNode.removeChild(temp);
653
- } else {
654
- formData = new FormData(form);
655
- }
637
+ const formData = new FormData(form, formDataSubmitter);
638
639
// Queue for replaying later. This field could potentially be shared with multiple
640
// Reacts on the same page since each one will preventDefault for the next one.
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+19
-5
@@ -14,8 +14,8 @@ global.IS_REACT_ACT_ENVIRONMENT = true;
14
// Our current version of JSDOM doesn't implement the event dispatching
15
// so we polyfill it.
16
const NativeFormData = global.FormData;
17
-const FormDataPolyfill = function FormData(form) {
18
- const formData = new NativeFormData(form);
17
+const FormDataPolyfill = function FormData(form, submitter) {
18
+ const formData = new NativeFormData(form, submitter);
19
const formDataEvent = new Event('formdata', {
20
bubbles: true,
21
cancelable: false,
@@ -489,11 +489,16 @@ describe('ReactDOMForm', () => {
489
const inputRef = React.createRef();
490
const buttonRef = React.createRef();
491
const outsideButtonRef = React.createRef();
492
+ const imageButtonRef = React.createRef();
493
let button;
494
+ let buttonX;
495
+ let buttonY;
496
let title;
497
498
function action(formData) {
499
button = formData.get('button');
500
+ buttonX = formData.get('button.x');
501
+ buttonY = formData.get('button.y');
502
title = formData.get('title');
503
}
504
@@ -508,6 +513,12 @@ describe('ReactDOMForm', () => {
513
<button name="button" value="edit" ref={buttonRef}>
514
Edit
515
</button>
516
+ <input
517
+ type="image"
518
+ name="button"
519
+ href="/some/image.png"
520
+ ref={imageButtonRef}
521
+ />
522
</form>
523
<form id="form" action={action}>
524
<input type="text" name="title" defaultValue="hello" />
@@ -546,9 +557,12 @@ describe('ReactDOMForm', () => {
557
expect(button).toBe('outside');
558
expect(title).toBe('hello');
559
549
- // Ensure that the type field got correctly restored
550
- expect(inputRef.current.getAttribute('type')).toBe('submit');
551
- expect(buttonRef.current.getAttribute('type')).toBe(null);
560
+ await submit(imageButtonRef.current);
561
+
562
+ expect(button).toBe(null);
563
+ expect(buttonX).toBe('0');
564
+ expect(buttonY).toBe('0');
565
+ expect(title).toBe('hello');
566
});
567
568
it('excludes the submitter name when the submitter is a function action', async () => {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
+1
-11
@@ -121,17 +121,7 @@ describe('ReactFlightDOMForm', () => {
121
const method = (submitter && submitter.formMethod) || form.method;
122
const encType = (submitter && submitter.formEnctype) || form.enctype;
123
if (method === 'post' && encType === 'multipart/form-data') {
124
- let formData;
125
- if (submitter) {
126
- const temp = document.createElement('input');
127
- temp.name = submitter.name;
128
- temp.value = submitter.value;
129
- submitter.parentNode.insertBefore(temp, submitter);
130
- formData = new FormData(form);
131
- temp.parentNode.removeChild(temp);
132
- } else {
133
- formData = new FormData(form);
134
- }
124
+ const formData = new FormData(form, submitter);
125
return POST(formData);
126
}
127
throw new Error('Navigate to: ' + action);