@samitouri / QOS-React / commits / 67f4fb0213

Allow forms to skip hydration of hidden inputs (#26735)

This allows us to emit extra ephemeral data that will only be used on server rendered forms. First I refactored the shouldSkip functions to now just do that work inside the canHydrate methods. This makes the Config bindings a little less surface area but it also helps us optimize a bit since we now can look at the code together and find shared paths. canHydrate returns the instance if it matches, that used to just be there to refine the type but it can also be used to just return a different instance later that we find. If we don't find one, we'll bail out and error regardless so no need to skip past anything.

Sebastian Markbåge committed May 1, 2023 at 15:35 UTC 67f4fb02130b1fe1856289e3b66bb0b8cca57ff7
5 files changed +202 -209
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+153 -128
@@ -89,6 +89,7 @@ import {
89 enableHostSingletons,
90 enableTrustedTypesIntegration,
91 diffInCommitPhase,
92 + enableFormActions,
93 } from 'shared/ReactFeatureFlags';
94 import {
95 HostComponent,
@@ -1038,150 +1039,164 @@ export function isHydratableText(text: string): boolean {
1039 return text !== '';
1040 }
1041
1041 -export function shouldSkipHydratableForInstance(
1042 +export function canHydrateInstance(
1043 instance: HydratableInstance,
1044 type: string,
1045 props: Props,
1045 -): boolean {
1046 - if (instance.nodeType !== ELEMENT_NODE) {
1047 - // This is a suspense boundary or Text node.
1048 - // Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
1049 - // and this is a hydration error.
1050 - // Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
1051 - // but it seems reasonable and conservative to reject this as a hydration error as well
1052 - return false;
1053 - } else if (
1054 - instance.nodeName.toLowerCase() !== type.toLowerCase() ||
1055 - isMarkedHoistable(instance)
1056 - ) {
1057 - // We are either about to
1058 - return true;
1059 - } else {
1060 - // We have an Element with the right type.
1046 + inRootOrSingleton: boolean,
1047 +): null | Instance {
1048 + while (instance.nodeType === ELEMENT_NODE) {
1049 const element: Element = (instance: any);
1050 const anyProps = (props: any);
1063 -
1064 - // We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
1065 - // we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
1066 - // using high entropy attributes for certain types. This technique will fail for strange insertions like
1067 - // extension prepending <div> in the <body> but that already breaks before and that is an edge case.
1068 - switch (type) {
1069 - // case 'title':
1070 - //We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
1071 - // and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
1072 - case 'meta': {
1073 - // The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
1074 - // not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
1075 - // are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
1076 - // implications are minimal
1077 - if (!element.hasAttribute('itemprop')) {
1078 - // This is a Hoistable
1079 - return true;
1080 - }
1081 - break;
1082 - }
1083 - case 'link': {
1084 - // Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
1085 - // and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
1086 - // matches.
1087 - const rel = element.getAttribute('rel');
1088 - if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
1089 - // This is a stylesheet resource
1090 - return true;
1091 - } else if (
1092 - rel !== anyProps.rel ||
1093 - element.getAttribute('href') !==
1094 - (anyProps.href == null ? null : anyProps.href) ||
1095 - element.getAttribute('crossorigin') !==
1096 - (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
1097 - element.getAttribute('title') !==
1098 - (anyProps.title == null ? null : anyProps.title)
1051 + if (element.nodeName.toLowerCase() !== type.toLowerCase()) {
1052 + if (!inRootOrSingleton || !enableHostSingletons) {
1053 + // Usually we error for mismatched tags.
1054 + if (
1055 + enableFormActions &&
1056 + element.nodeName === 'INPUT' &&
1057 + (element: any).type === 'hidden'
1058 ) {
1100 - // rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
1101 - // and title could vary for rel alternate
1102 - return true;
1059 + // If we have extra hidden inputs, we don't mismatch. This allows us to embed
1060 + // extra form data in the original form.
1061 + } else {
1062 + return null;
1063 }
1104 - break;
1064 }
1106 - case 'style': {
1107 - // Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
1108 - // in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
1109 - if (element.hasAttribute('data-precedence')) {
1110 - // This is a style resource
1111 - return true;
1112 - }
1113 - break;
1065 + // In root or singleton parents we skip past mismatched instances.
1066 + } else if (!inRootOrSingleton || !enableHostSingletons) {
1067 + // Match
1068 + if (
1069 + enableFormActions &&
1070 + type === 'input' &&
1071 + (element: any).type === 'hidden' &&
1072 + anyProps.type !== 'hidden'
1073 + ) {
1074 + // Skip past hidden inputs unless that's what we're looking for. This allows us
1075 + // embed extra form data in the original form.
1076 + } else {
1077 + return element;
1078 }
1115 - case 'script': {
1116 - // Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
1117 - // to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
1118 - // in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
1119 - // Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
1120 - // if we learn it is problematic
1121 - const srcAttr = element.getAttribute('src');
1122 - if (
1123 - srcAttr &&
1124 - element.hasAttribute('async') &&
1125 - !element.hasAttribute('itemprop')
1126 - ) {
1127 - // This is an async script resource
1128 - return true;
1129 - } else if (
1130 - srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
1131 - element.getAttribute('type') !==
1132 - (anyProps.type == null ? null : anyProps.type) ||
1133 - element.getAttribute('crossorigin') !==
1134 - (anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
1135 - ) {
1136 - // This script is for a different src
1137 - return true;
1079 + } else if (isMarkedHoistable(element)) {
1080 + // We've already claimed this as a hoistable which isn't hydrated this way so we skip past it.
1081 + } else {
1082 + // We have an Element with the right type.
1083 +
1084 + // We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
1085 + // we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
1086 + // using high entropy attributes for certain types. This technique will fail for strange insertions like
1087 + // extension prepending <div> in the <body> but that already breaks before and that is an edge case.
1088 + switch (type) {
1089 + // case 'title':
1090 + //We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
1091 + // and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
1092 + case 'meta': {
1093 + // The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
1094 + // not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
1095 + // are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
1096 + // implications are minimal
1097 + if (!element.hasAttribute('itemprop')) {
1098 + // This is a Hoistable
1099 + break;
1100 + }
1101 + return element;
1102 + }
1103 + case 'link': {
1104 + // Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
1105 + // and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
1106 + // matches.
1107 + const rel = element.getAttribute('rel');
1108 + if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
1109 + // This is a stylesheet resource
1110 + break;
1111 + } else if (
1112 + rel !== anyProps.rel ||
1113 + element.getAttribute('href') !==
1114 + (anyProps.href == null ? null : anyProps.href) ||
1115 + element.getAttribute('crossorigin') !==
1116 + (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
1117 + element.getAttribute('title') !==
1118 + (anyProps.title == null ? null : anyProps.title)
1119 + ) {
1120 + // rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
1121 + // and title could vary for rel alternate
1122 + break;
1123 + }
1124 + return element;
1125 + }
1126 + case 'style': {
1127 + // Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
1128 + // in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
1129 + if (element.hasAttribute('data-precedence')) {
1130 + // This is a style resource
1131 + break;
1132 + }
1133 + return element;
1134 + }
1135 + case 'script': {
1136 + // Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
1137 + // to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
1138 + // in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
1139 + // Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
1140 + // if we learn it is problematic
1141 + const srcAttr = element.getAttribute('src');
1142 + if (
1143 + srcAttr &&
1144 + element.hasAttribute('async') &&
1145 + !element.hasAttribute('itemprop')
1146 + ) {
1147 + // This is an async script resource
1148 + break;
1149 + } else if (
1150 + srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
1151 + element.getAttribute('type') !==
1152 + (anyProps.type == null ? null : anyProps.type) ||
1153 + element.getAttribute('crossorigin') !==
1154 + (anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
1155 + ) {
1156 + // This script is for a different src
1157 + break;
1158 + }
1159 + return element;
1160 + }
1161 + default: {
1162 + // We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
1163 + // and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
1164 + // that should work in the vast majority of cases.
1165 + return element;
1166 }
1139 - break;
1167 }
1168 }
1142 - // We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
1143 - // and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
1144 - // that should work in the vast majority of cases.
1145 - return false;
1146 - }
1147 -}
1148 -
1149 -export function shouldSkipHydratableForTextInstance(
1150 - instance: HydratableInstance,
1151 -): boolean {
1152 - return instance.nodeType === ELEMENT_NODE;
1153 -}
1154 -
1155 -export function shouldSkipHydratableForSuspenseInstance(
1156 - instance: HydratableInstance,
1157 -): boolean {
1158 - return instance.nodeType === ELEMENT_NODE;
1159 -}
1160 -
1161 -export function canHydrateInstance(
1162 - instance: HydratableInstance,
1163 - type: string,
1164 - props: Props,
1165 -): null | Instance {
1166 - if (
1167 - instance.nodeType !== ELEMENT_NODE ||
1168 - instance.nodeName.toLowerCase() !== type.toLowerCase()
1169 - ) {
1170 - return null;
1171 - } else {
1172 - return ((instance: any): Instance);
1169 + const nextInstance = getNextHydratableSibling(element);
1170 + if (nextInstance === null) {
1171 + break;
1172 + }
1173 + instance = nextInstance;
1174 }
1175 + // This is a suspense boundary or Text node or we got the end.
1176 + // Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
1177 + // and this is a hydration error.
1178 + // Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
1179 + // but it seems reasonable and conservative to reject this as a hydration error as well
1180 + return null;
1181 }
1182
1183 export function canHydrateTextInstance(
1184 instance: HydratableInstance,
1185 text: string,
1186 + inRootOrSingleton: boolean,
1187 ): null | TextInstance {
1188 + // Empty strings are not parsed by HTML so there won't be a correct match here.
1189 if (text === '') return null;
1190
1182 - if (instance.nodeType !== TEXT_NODE) {
1183 - // Empty strings are not parsed by HTML so there won't be a correct match here.
1184 - return null;
1191 + while (instance.nodeType !== TEXT_NODE) {
1192 + if (!inRootOrSingleton || !enableHostSingletons) {
1193 + return null;
1194 + }
1195 + const nextInstance = getNextHydratableSibling(instance);
1196 + if (nextInstance === null) {
1197 + return null;
1198 + }
1199 + instance = nextInstance;
1200 }
1201 // This has now been refined to a text node.
1202 return ((instance: any): TextInstance);
@@ -1189,9 +1204,17 @@ export function canHydrateTextInstance(
1204
1205 export function canHydrateSuspenseInstance(
1206 instance: HydratableInstance,
1207 + inRootOrSingleton: boolean,
1208 ): null | SuspenseInstance {
1193 - if (instance.nodeType !== COMMENT_NODE) {
1194 - return null;
1209 + while (instance.nodeType !== COMMENT_NODE) {
1210 + if (!inRootOrSingleton || !enableHostSingletons) {
1211 + return null;
1212 + }
1213 + const nextInstance = getNextHydratableSibling(instance);
1214 + if (nextInstance === null) {
1215 + return null;
1216 + }
1217 + instance = nextInstance;
1218 }
1219 // This has now been refined to a suspense node.
1220 return ((instance: any): SuspenseInstance);
@@ -1416,12 +1439,14 @@ export function commitHydratedSuspenseInstance(
1439 retryIfBlockedOn(suspenseInstance);
1440 }
1441
1419 -// @TODO remove this function once float lands and hydrated tail nodes
1420 -// are controlled by HostSingleton fibers
1442 export function shouldDeleteUnhydratedTailInstances(
1443 parentType: string,
1444 ): boolean {
1424 - return parentType !== 'head' && parentType !== 'body';
1445 + return (
1446 + (enableHostSingletons ||
1447 + (parentType !== 'head' && parentType !== 'body')) &&
1448 + (!enableFormActions || parentType !== 'form')
1449 + );
1450 }
1451
1452 export function didNotMatchHydratedContainerTextInstance(
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+37
@@ -695,4 +695,41 @@ describe('ReactDOMServerHydration', () => {
695 );
696 }
697 });
698 +
699 + // @gate enableFormActions
700 + it('allows rendering extra hidden inputs in a form', async () => {
701 + const element = document.createElement('div');
702 + element.innerHTML =
703 + '<form>' +
704 + '<input type="hidden" /><input type="hidden" name="a" value="A" />' +
705 + '<input type="hidden" /><input type="submit" name="b" value="B" />' +
706 + '<input type="hidden" /><button name="c" value="C"></button>' +
707 + '<input type="hidden" />' +
708 + '</form>';
709 + const form = element.firstChild;
710 + const ref = React.createRef();
711 + const a = React.createRef();
712 + const b = React.createRef();
713 + const c = React.createRef();
714 + await act(async () => {
715 + ReactDOMClient.hydrateRoot(
716 + element,
717 + <form ref={ref}>
718 + <input type="hidden" name="a" value="A" ref={a} />
719 + <input type="submit" name="b" value="B" ref={b} />
720 + <button name="c" value="C" ref={c} />
721 + </form>,
722 + );
723 + });
724 +
725 + // The content should not have been client rendered.
726 + expect(ref.current).toBe(form);
727 +
728 + expect(a.current.name).toBe('a');
729 + expect(a.current.value).toBe('A');
730 + expect(b.current.name).toBe('b');
731 + expect(b.current.value).toBe('B');
732 + expect(c.current.name).toBe('c');
733 + expect(c.current.value).toBe('C');
734 + });
735 });
packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
-3
@@ -31,9 +31,6 @@ export const getNextHydratableSibling = shim;
31 export const getFirstHydratableChild = shim;
32 export const getFirstHydratableChildWithinContainer = shim;
33 export const getFirstHydratableChildWithinSuspenseInstance = shim;
34 -export const shouldSkipHydratableForInstance = shim;
35 -export const shouldSkipHydratableForTextInstance = shim;
36 -export const shouldSkipHydratableForSuspenseInstance = shim;
34 export const canHydrateInstance = shim;
35 export const canHydrateTextInstance = shim;
36 export const canHydrateSuspenseInstance = shim;
packages/react-reconciler/src/ReactFiberHydrationContext.js
+12 -72
@@ -74,9 +74,6 @@ import {
74 didNotFindHydratableTextInstance,
75 didNotFindHydratableSuspenseInstance,
76 resolveSingletonInstance,
77 - shouldSkipHydratableForInstance,
78 - shouldSkipHydratableForTextInstance,
79 - shouldSkipHydratableForSuspenseInstance,
77 canHydrateInstance,
78 canHydrateTextInstance,
79 canHydrateSuspenseInstance,
@@ -355,6 +352,7 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
352 nextInstance,
353 fiber.type,
354 fiber.pendingProps,
355 + rootOrSingletonContext,
356 );
357 if (instance !== null) {
358 fiber.stateNode = (instance: Instance);
@@ -369,7 +367,11 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
367 function tryHydrateText(fiber: Fiber, nextInstance: any) {
368 // fiber is a HostText Fiber
369 const text = fiber.pendingProps;
372 - const textInstance = canHydrateTextInstance(nextInstance, text);
370 + const textInstance = canHydrateTextInstance(
371 + nextInstance,
372 + text,
373 + rootOrSingletonContext,
374 + );
375 if (textInstance !== null) {
376 fiber.stateNode = (textInstance: TextInstance);
377 hydrationParentFiber = fiber;
@@ -382,7 +384,10 @@ function tryHydrateText(fiber: Fiber, nextInstance: any) {
384
385 function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
386 // fiber is a SuspenseComponent Fiber
385 - const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
387 + const suspenseInstance = canHydrateSuspenseInstance(
388 + nextInstance,
389 + rootOrSingletonContext,
390 + );
391 if (suspenseInstance !== null) {
392 const suspenseState: SuspenseState = {
393 dehydrated: suspenseInstance,
@@ -441,44 +446,6 @@ function claimHydratableSingleton(fiber: Fiber): void {
446 }
447 }
448
444 -function advanceToFirstAttemptableInstance(fiber: Fiber) {
445 - // fiber is HostComponent Fiber
446 - while (
447 - nextHydratableInstance &&
448 - shouldSkipHydratableForInstance(
449 - nextHydratableInstance,
450 - fiber.type,
451 - fiber.pendingProps,
452 - )
453 - ) {
454 - // Flow doesn't understand that inside this block nextHydratableInstance is not null
455 - const instance: HydratableInstance = (nextHydratableInstance: any);
456 - nextHydratableInstance = getNextHydratableSibling(instance);
457 - }
458 -}
459 -
460 -function advanceToFirstAttemptableTextInstance() {
461 - while (
462 - nextHydratableInstance &&
463 - shouldSkipHydratableForTextInstance(nextHydratableInstance)
464 - ) {
465 - // Flow doesn't understand that inside this block nextHydratableInstance is not null
466 - const instance: HydratableInstance = (nextHydratableInstance: any);
467 - nextHydratableInstance = getNextHydratableSibling(instance);
468 - }
469 -}
470 -
471 -function advanceToFirstAttemptableSuspenseInstance() {
472 - while (
473 - nextHydratableInstance &&
474 - shouldSkipHydratableForSuspenseInstance(nextHydratableInstance)
475 - ) {
476 - // Flow doesn't understand that inside this block nextHydratableInstance is not null
477 - const instance: HydratableInstance = (nextHydratableInstance: any);
478 - nextHydratableInstance = getNextHydratableSibling(instance);
479 - }
480 -}
481 -
449 function tryToClaimNextHydratableInstance(fiber: Fiber): void {
450 if (!isHydrating) {
451 return;
@@ -493,10 +460,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
460 }
461 }
462 const initialInstance = nextHydratableInstance;
496 - if (rootOrSingletonContext) {
497 - // We may need to skip past certain nodes in these contexts
498 - advanceToFirstAttemptableInstance(fiber);
499 - }
463 const nextInstance = nextHydratableInstance;
464 if (!nextInstance) {
465 if (shouldClientRenderOnMismatch(fiber)) {
@@ -521,10 +484,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
484 // might be flawed or unnecessary.
485 nextHydratableInstance = getNextHydratableSibling(nextInstance);
486 const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
524 - if (rootOrSingletonContext) {
525 - // We may need to skip past certain nodes in these contexts
526 - advanceToFirstAttemptableInstance(fiber);
527 - }
487 if (
488 !nextHydratableInstance ||
489 !tryHydrateInstance(fiber, nextHydratableInstance)
@@ -552,12 +511,6 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
511 const isHydratable = isHydratableText(text);
512
513 const initialInstance = nextHydratableInstance;
555 - if (rootOrSingletonContext && isHydratable) {
556 - // We may need to skip past certain nodes in these contexts.
557 - // We don't skip if the text is not hydratable because we know no hydratables
558 - // exist which could match this Fiber
559 - advanceToFirstAttemptableTextInstance();
560 - }
514 const nextInstance = nextHydratableInstance;
515 if (!nextInstance || !isHydratable) {
516 // We exclude non hydrabable text because we know there are no matching hydratables.
@@ -585,11 +538,6 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
538 nextHydratableInstance = getNextHydratableSibling(nextInstance);
539 const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
540
588 - if (rootOrSingletonContext && isHydratable) {
589 - // We may need to skip past certain nodes in these contexts
590 - advanceToFirstAttemptableTextInstance();
591 - }
592 -
541 if (
542 !nextHydratableInstance ||
543 !tryHydrateText(fiber, nextHydratableInstance)
@@ -614,10 +562,6 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
562 return;
563 }
564 const initialInstance = nextHydratableInstance;
617 - if (rootOrSingletonContext) {
618 - // We may need to skip past certain nodes in these contexts
619 - advanceToFirstAttemptableSuspenseInstance();
620 - }
565 const nextInstance = nextHydratableInstance;
566 if (!nextInstance) {
567 if (shouldClientRenderOnMismatch(fiber)) {
@@ -643,11 +587,6 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
587 nextHydratableInstance = getNextHydratableSibling(nextInstance);
588 const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
589
646 - if (rootOrSingletonContext) {
647 - // We may need to skip past certain nodes in these contexts
648 - advanceToFirstAttemptableSuspenseInstance();
649 - }
650 -
590 if (
591 !nextHydratableInstance ||
592 !tryHydrateSuspense(fiber, nextHydratableInstance)
@@ -863,7 +802,8 @@ function popHydrationState(fiber: Fiber): boolean {
802 fiber.tag !== HostSingleton &&
803 !(
804 fiber.tag === HostComponent &&
866 - shouldSetTextContent(fiber.type, fiber.memoizedProps)
805 + (!shouldDeleteUnhydratedTailInstances(fiber.type) ||
806 + shouldSetTextContent(fiber.type, fiber.memoizedProps))
807 )
808 ) {
809 shouldClear = true;
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
-6
@@ -149,12 +149,6 @@ export const getFirstHydratableChildWithinContainer =
149 $$$config.getFirstHydratableChildWithinContainer;
150 export const getFirstHydratableChildWithinSuspenseInstance =
151 $$$config.getFirstHydratableChildWithinSuspenseInstance;
152 -export const shouldSkipHydratableForInstance =
153 - $$$config.shouldSkipHydratableForInstance;
154 -export const shouldSkipHydratableForTextInstance =
155 - $$$config.shouldSkipHydratableForTextInstance;
156 -export const shouldSkipHydratableForSuspenseInstance =
157 - $$$config.shouldSkipHydratableForSuspenseInstance;
152 export const canHydrateInstance = $$$config.canHydrateInstance;
153 export const canHydrateTextInstance = $$$config.canHydrateTextInstance;
154 export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance;