@@ -89,6 +89,7 @@ import {
89
89
enableHostSingletons ,
90
90
enableTrustedTypesIntegration ,
91
91
diffInCommitPhase ,
92
+ enableFormActions ,
92
93
} from 'shared/ReactFeatureFlags' ;
93
94
import {
94
95
HostComponent ,
@@ -1038,160 +1039,182 @@ export function isHydratableText(text: string): boolean {
1038
1039
return text !== '' ;
1039
1040
}
1040
1041
1041
- export function shouldSkipHydratableForInstance (
1042
+ export function canHydrateInstance (
1042
1043
instance : HydratableInstance ,
1043
1044
type : string ,
1044
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 ) {
1061
1049
const element : Element = ( instance : any ) ;
1062
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'
1099
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 ;
1103
1063
}
1104
- break ;
1105
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 ;
1114
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 ;
1138
1166
}
1139
- break ;
1140
1167
}
1141
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 ;
1173
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 ;
1174
1181
}
1175
1182
1176
1183
export function canHydrateTextInstance (
1177
1184
instance : HydratableInstance ,
1178
1185
text : string ,
1186
+ inRootOrSingleton : boolean ,
1179
1187
) : null | TextInstance {
1188
+ // Empty strings are not parsed by HTML so there won't be a correct match here.
1180
1189
if ( text === '' ) return null ;
1181
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 ;
1185
1200
}
1186
1201
// This has now been refined to a text node.
1187
1202
return ( ( instance : any ) : TextInstance ) ;
1188
1203
}
1189
1204
1190
1205
export function canHydrateSuspenseInstance (
1191
1206
instance : HydratableInstance ,
1207
+ inRootOrSingleton : boolean ,
1192
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 ;
1195
1218
}
1196
1219
// This has now been refined to a suspense node.
1197
1220
return ( ( instance : any ) : SuspenseInstance ) ;
@@ -1416,12 +1439,14 @@ export function commitHydratedSuspenseInstance(
1416
1439
retryIfBlockedOn ( suspenseInstance ) ;
1417
1440
}
1418
1441
1419
- // @TODO remove this function once float lands and hydrated tail nodes
1420
- // are controlled by HostSingleton fibers
1421
1442
export function shouldDeleteUnhydratedTailInstances (
1422
1443
parentType : string ,
1423
1444
) : boolean {
1424
- return parentType !== 'head' && parentType !== 'body' ;
1445
+ return (
1446
+ ( enableHostSingletons ||
1447
+ ( parentType !== 'head' && parentType !== 'body' ) ) &&
1448
+ ( ! enableFormActions || parentType !== 'form' )
1449
+ ) ;
1425
1450
}
1426
1451
1427
1452
export function didNotMatchHydratedContainerTextInstance (
0 commit comments