@@ -89,6 +89,7 @@ import {
8989 enableHostSingletons ,
9090 enableTrustedTypesIntegration ,
9191 diffInCommitPhase ,
92+ enableFormActions ,
9293} from 'shared/ReactFeatureFlags' ;
9394import {
9495 HostComponent ,
@@ -1038,160 +1039,182 @@ export function isHydratableText(text: string): boolean {
10381039 return text !== '' ;
10391040}
10401041
1041- export function shouldSkipHydratableForInstance (
1042+ export function canHydrateInstance (
10421043 instance : HydratableInstance ,
10431044 type : string ,
10441045 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 ) {
10611049 const element : Element = ( instance : any ) ;
10621050 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'
10991058 ) {
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 ;
11031063 }
1104- break ;
11051064 }
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 ;
11141078 }
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 ;
11381166 }
1139- break ;
11401167 }
11411168 }
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 ;
11731174 }
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 ;
11741181}
11751182
11761183export function canHydrateTextInstance (
11771184 instance : HydratableInstance ,
11781185 text : string ,
1186+ inRootOrSingleton : boolean ,
11791187) : null | TextInstance {
1188+ // Empty strings are not parsed by HTML so there won't be a correct match here.
11801189 if ( text === '' ) return null ;
11811190
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 ;
11851200 }
11861201 // This has now been refined to a text node.
11871202 return ( ( instance : any ) : TextInstance ) ;
11881203}
11891204
11901205export function canHydrateSuspenseInstance (
11911206 instance : HydratableInstance ,
1207+ inRootOrSingleton : boolean ,
11921208) : 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 ;
11951218 }
11961219 // This has now been refined to a suspense node.
11971220 return ( ( instance : any ) : SuspenseInstance ) ;
@@ -1416,12 +1439,14 @@ export function commitHydratedSuspenseInstance(
14161439 retryIfBlockedOn ( suspenseInstance ) ;
14171440}
14181441
1419- // @TODO remove this function once float lands and hydrated tail nodes
1420- // are controlled by HostSingleton fibers
14211442export function shouldDeleteUnhydratedTailInstances (
14221443 parentType : string ,
14231444) : boolean {
1424- return parentType !== 'head' && parentType !== 'body' ;
1445+ return (
1446+ ( enableHostSingletons ||
1447+ ( parentType !== 'head' && parentType !== 'body' ) ) &&
1448+ ( ! enableFormActions || parentType !== 'form' )
1449+ ) ;
14251450}
14261451
14271452export function didNotMatchHydratedContainerTextInstance (
0 commit comments