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

Fix: Skip hidden inputs before text instance (#27358)

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.

Andrew Clark committed Sep 11, 2023 at 18:22 UTC a4aceafc63a4a54b507ab60d530b25d9ff189024
2 files changed +28 -1
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+9 -1
@@ -1216,7 +1216,15 @@ export function canHydrateTextInstance(
1216 if (text === '') return null;
1217
1218 while (instance.nodeType !== TEXT_NODE) {
1219 - if (!inRootOrSingleton || !enableHostSingletons) {
1219 + if (
1220 + enableFormActions &&
1221 + instance.nodeType === ELEMENT_NODE &&
1222 + instance.nodeName === 'INPUT' &&
1223 + (instance: any).type === 'hidden'
1224 + ) {
1225 + // If we have extra hidden inputs, we don't mismatch. This allows us to
1226 + // embed extra form data in the original form.
1227 + } else if (!inRootOrSingleton || !enableHostSingletons) {
1228 return null;
1229 }
1230 const nextInstance = getNextHydratableSibling(instance);
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+19
@@ -732,4 +732,23 @@ describe('ReactDOMServerHydration', () => {
732 expect(c.current.name).toBe('c');
733 expect(c.current.value).toBe('C');
734 });
735 +
736 + // @gate enableFormActions
737 + it('allows rendering extra hidden inputs immediately before a text instance', async () => {
738 + const element = document.createElement('div');
739 + element.innerHTML =
740 + '<button><input name="a" value="A" type="hidden" />Click <!-- -->me</button>';
741 + const button = element.firstChild;
742 + const ref = React.createRef();
743 + const extraText = 'me';
744 +
745 + await act(() => {
746 + ReactDOMClient.hydrateRoot(
747 + element,
748 + <button ref={ref}>Click {extraText}</button>,
749 + );
750 + });
751 +
752 + expect(ref.current).toBe(button);
753 + });
754 });