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
This commit is contained in:
Sebastian Markbåge
2023-05-26 12:54:01 -04:00
committed by GitHub
parent 0210f0b082
commit a1f97589fd
2 changed files with 67 additions and 5 deletions
@@ -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;
}
+53 -1
View File
@@ -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 (
<form>
<input type="hidden" name="foo" value="bar" />
<input type="hidden" name="foo" value="bar" ref={hiddenRef} />
<input
type="submit"
formAction={action}
@@ -588,6 +589,8 @@ describe('ReactDOMFizzForm', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
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 (
<form action={action}>
<input type="hidden" name="bar" defaultValue="baz" ref={hiddenRef} />
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const stream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(stream);
const barField = container.querySelector('[name=bar]');
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
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);
});
});