Compare name when hydrating hidden fields to filter out extra form action fields (#26846)
This solves an issue where if you inject a hidden field in the beginning of the form, we might mistakenly hydrate the injected one that was part of an action. I'm not too happy about how specific this becomes. It's similar to Float but in general we don't do this deep comparison. See https://github.com/vercel/next.js/issues/50087
Sebastian Markbåge committed
May 26, 2023 at 12:54 UTC
a1f97589fd298cd71f97339a230f016139c7382f
2 files changed
+67
-5
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+14
-4
@@ -1068,11 +1068,21 @@ export function canHydrateInstance(
1068
if (
1069
enableFormActions &&
1070
type === 'input' &&
1071
- (element: any).type === 'hidden' &&
1072
- anyProps.type !== 'hidden'
1071
+ (element: any).type === 'hidden'
1072
) {
1074
- // Skip past hidden inputs unless that's what we're looking for. This allows us
1075
- // embed extra form data in the original form.
1073
+ if (__DEV__) {
1074
+ checkAttributeStringCoercion(anyProps.name, 'name');
1075
+ }
1076
+ const name = anyProps.name == null ? null : '' + anyProps.name;
1077
+ if (
1078
+ anyProps.type !== 'hidden' ||
1079
+ element.getAttribute('name') !== name
1080
+ ) {
1081
+ // Skip past hidden inputs unless that's what we're looking for. This allows us
1082
+ // embed extra form data in the original form.
1083
+ } else {
1084
+ return element;
1085
+ }
1086
} else {
1087
return element;
1088
}
packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
+53
-1
@@ -524,6 +524,7 @@ describe('ReactDOMFizzForm', () => {
524
525
// @gate enableFormActions
526
it('can provide a custom action on buttons the server for actions', async () => {
527
+ const hiddenRef = React.createRef();
528
const inputRef = React.createRef();
529
const buttonRef = React.createRef();
530
let foo;
@@ -546,7 +547,7 @@ describe('ReactDOMFizzForm', () => {
547
function App() {
548
return (
549
<form>
549
- <input type="hidden" name="foo" value="bar" />
550
+ <input type="hidden" name="foo" value="bar" ref={hiddenRef} />
551
<input
552
type="submit"
553
formAction={action}
@@ -588,6 +589,8 @@ describe('ReactDOMFizzForm', () => {
589
ReactDOMClient.hydrateRoot(container, <App />);
590
});
591
592
+ expect(hiddenRef.current.name).toBe('foo');
593
+
594
submit(inputRef.current);
595
596
expect(foo).toBe('bar');
@@ -598,4 +601,53 @@ describe('ReactDOMFizzForm', () => {
601
602
expect(foo).toBe('bar');
603
});
604
+
605
+ // @gate enableFormActions
606
+ it('can hydrate hidden fields in the beginning of a form', async () => {
607
+ const hiddenRef = React.createRef();
608
+
609
+ let invoked = false;
610
+ function action(formData) {
611
+ invoked = true;
612
+ }
613
+ action.$$FORM_ACTION = function (identifierPrefix) {
614
+ const extraFields = new FormData();
615
+ extraFields.append(identifierPrefix + 'hello', 'world');
616
+ return {
617
+ action: '',
618
+ name: identifierPrefix,
619
+ method: 'POST',
620
+ encType: 'multipart/form-data',
621
+ data: extraFields,
622
+ };
623
+ };
624
+ function App() {
625
+ return (
626
+ <form action={action}>
627
+ <input type="hidden" name="bar" defaultValue="baz" ref={hiddenRef} />
628
+ <input type="text" name="foo" defaultValue="bar" />
629
+ </form>
630
+ );
631
+ }
632
+
633
+ const stream = await ReactDOMServer.renderToReadableStream(<App />);
634
+ await readIntoContainer(stream);
635
+
636
+ const barField = container.querySelector('[name=bar]');
637
+
638
+ await act(async () => {
639
+ ReactDOMClient.hydrateRoot(container, <App />);
640
+ });
641
+
642
+ expect(hiddenRef.current).toBe(barField);
643
+
644
+ expect(hiddenRef.current.name).toBe('bar');
645
+ expect(hiddenRef.current.value).toBe('baz');
646
+
647
+ expect(container.querySelectorAll('[name=bar]').length).toBe(1);
648
+
649
+ submit(hiddenRef.current.form);
650
+
651
+ expect(invoked).toBe(true);
652
+ });
653
});