From a1f97589fd298cd71f97339a230f016139c7382f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 26 May 2023 12:54:01 -0400 Subject: [PATCH] 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 --- .../src/client/ReactFiberConfigDOM.js | 18 +++++-- .../src/__tests__/ReactDOMFizzForm-test.js | 54 ++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 363048f070..11c290d148 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1068,11 +1068,21 @@ export function canHydrateInstance( if ( enableFormActions && type === 'input' && - (element: any).type === 'hidden' && - anyProps.type !== 'hidden' + (element: any).type === 'hidden' ) { - // Skip past hidden inputs unless that's what we're looking for. This allows us - // embed extra form data in the original form. + if (__DEV__) { + checkAttributeStringCoercion(anyProps.name, 'name'); + } + const name = anyProps.name == null ? null : '' + anyProps.name; + if ( + anyProps.type !== 'hidden' || + element.getAttribute('name') !== name + ) { + // Skip past hidden inputs unless that's what we're looking for. This allows us + // embed extra form data in the original form. + } else { + return element; + } } else { return element; } diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index 6487b48d03..b13eb90304 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -524,6 +524,7 @@ describe('ReactDOMFizzForm', () => { // @gate enableFormActions it('can provide a custom action on buttons the server for actions', async () => { + const hiddenRef = React.createRef(); const inputRef = React.createRef(); const buttonRef = React.createRef(); let foo; @@ -546,7 +547,7 @@ describe('ReactDOMFizzForm', () => { function App() { return (
- + { ReactDOMClient.hydrateRoot(container, ); }); + expect(hiddenRef.current.name).toBe('foo'); + submit(inputRef.current); expect(foo).toBe('bar'); @@ -598,4 +601,53 @@ describe('ReactDOMFizzForm', () => { expect(foo).toBe('bar'); }); + + // @gate enableFormActions + it('can hydrate hidden fields in the beginning of a form', async () => { + const hiddenRef = React.createRef(); + + let invoked = false; + function action(formData) { + invoked = true; + } + action.$$FORM_ACTION = function (identifierPrefix) { + const extraFields = new FormData(); + extraFields.append(identifierPrefix + 'hello', 'world'); + return { + action: '', + name: identifierPrefix, + method: 'POST', + encType: 'multipart/form-data', + data: extraFields, + }; + }; + function App() { + return ( + + + + + ); + } + + const stream = await ReactDOMServer.renderToReadableStream(); + await readIntoContainer(stream); + + const barField = container.querySelector('[name=bar]'); + + await act(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + + expect(hiddenRef.current).toBe(barField); + + expect(hiddenRef.current.name).toBe('bar'); + expect(hiddenRef.current.value).toBe('baz'); + + expect(container.querySelectorAll('[name=bar]').length).toBe(1); + + submit(hiddenRef.current.form); + + expect(invoked).toBe(true); + }); });