89
enableHostSingletons,
90
enableTrustedTypesIntegration,
91
diffInCommitPhase,
92
+ enableFormActions,
93
} from 'shared/ReactFeatureFlags';
94
import {
95
HostComponent,
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);
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);
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(