@samitouri / QOS-React-2 / commits / a1f97589fd

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 });