Skip to content

Commit

Permalink
Fix: Skip hidden inputs before text instance (#27358)
Browse files Browse the repository at this point in the history
Found a hydration bug that happens when you pass a Server Action to
`formAction` and the next node is a text instance.

The HTML generated by Fizz is something like this:

```html
<button name="$ACTION_REF_5" formAction="" formEncType="multipart/form-data" formMethod="POST">
  <input type="hidden" name="$ACTION_5:0" value="..."/>
  <input type="hidden" name="$ACTION_5:1" value="..."/>
  <input type="hidden" name="$ACTION_KEY" value="..."/>Count: <!-- -->0
</button>
```

Fiber is supposed to skip over the extra hidden inputs, but it doesn't
handle this correctly if the next expected node isn't a host instance.
In this case, it's a text instance.

Not sure if the proper fix is to change the HTML that is generated, or
to change the hydration logic, but in this PR I've done the latter.
  • Loading branch information
acdlite authored Sep 11, 2023
1 parent 627b7ab commit a4aceaf
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 1 deletion.
10 changes: 9 additions & 1 deletion packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,15 @@ export function canHydrateTextInstance(
if (text === '') return null;

while (instance.nodeType !== TEXT_NODE) {
if (!inRootOrSingleton || !enableHostSingletons) {
if (
enableFormActions &&
instance.nodeType === ELEMENT_NODE &&
instance.nodeName === 'INPUT' &&
(instance: any).type === 'hidden'
) {
// If we have extra hidden inputs, we don't mismatch. This allows us to
// embed extra form data in the original form.
} else if (!inRootOrSingleton || !enableHostSingletons) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -732,4 +732,23 @@ describe('ReactDOMServerHydration', () => {
expect(c.current.name).toBe('c');
expect(c.current.value).toBe('C');
});

// @gate enableFormActions
it('allows rendering extra hidden inputs immediately before a text instance', async () => {
const element = document.createElement('div');
element.innerHTML =
'<button><input name="a" value="A" type="hidden" />Click <!-- -->me</button>';
const button = element.firstChild;
const ref = React.createRef();
const extraText = 'me';

await act(() => {
ReactDOMClient.hydrateRoot(
element,
<button ref={ref}>Click {extraText}</button>,
);
});

expect(ref.current).toBe(button);
});
});

0 comments on commit a4aceaf

Please sign in to comment.