Skip to content

Commit

Permalink
Compare name when hydrating hidden fields to filter out extra form ac…
Browse files Browse the repository at this point in the history
…tion fields (facebook#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 vercel/next.js#50087
  • Loading branch information
sebmarkbage authored and AndyPengc12 committed Apr 15, 2024
1 parent fd1c0b2 commit f9533ef
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 5 deletions.
18 changes: 14 additions & 4 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
54 changes: 53 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
Expand Down Expand Up @@ -588,6 +589,8 @@ describe('ReactDOMFizzForm', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});

expect(hiddenRef.current.name).toBe('foo');

submit(inputRef.current);

expect(foo).toBe('bar');
Expand All @@ -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);
});
});

0 comments on commit f9533ef

Please sign in to comment.