Skip to content

Commit eca3420

Browse files
committed
refactor hydration attempts to move all hydration pointer state to Fiber
1 parent 1a4115e commit eca3420

File tree

5 files changed

+408
-289
lines changed

5 files changed

+408
-289
lines changed

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

Lines changed: 137 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,7 @@ export const supportsHydration = true;
847847

848848
// With Resources, some HostComponent types will never be server rendered and need to be
849849
// inserted without breaking hydration
850-
export function isHydratable(type: string, props: Props): boolean {
850+
export function isHydratableType(type: string, props: Props): boolean {
851851
if (enableFloat) {
852852
if (type === 'script') {
853853
const {async, onLoad, onError} = (props: any);
@@ -858,211 +858,167 @@ export function isHydratable(type: string, props: Props): boolean {
858858
return true;
859859
}
860860
}
861-
862-
// In certain contexts, namely <body> and <head>, we want to skip past Nodes that are in theory
863-
// hydratable but do not match the current Fiber being hydrated. We track the hydratable node we
864-
// are currently attempting using this module global. If the hydration is unsuccessful Fiber will
865-
// call getLastAttemptedHydratable which uses this cursor to return the expected next
866-
// hydratable.
867-
let hydratableNode: null | HydratableInstance = null;
868-
869-
export function getLastAttemptedHydratable(): null | HydratableInstance {
870-
return hydratableNode;
861+
export function isHydratableText(text: string): boolean {
862+
return text !== '';
871863
}
872864

873-
export function getNextMatchingHydratableInstance(
865+
export function shouldSkipHydratableForInstance(
874866
instance: HydratableInstance,
875867
type: string,
876868
props: Props,
877-
rootOrSingletonContext: boolean,
878-
): null | Instance {
879-
const anyProps = (props: any);
880-
// We set this first because it must always be set on every invocation
881-
hydratableNode = instance;
882-
if (rootOrSingletonContext) {
883-
// In the head and body we expect 3rd party scripts to
884-
let node;
885-
for (; hydratableNode; hydratableNode = getNextHydratableSibling(node)) {
886-
node = hydratableNode;
887-
if (node.nodeType !== ELEMENT_NODE) {
888-
// This is a suspense boundary or Text node.
889-
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
890-
// and this is a hydration error.
891-
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
892-
// but it seems reasonable and conservative to reject this as a hydration error as well
893-
return null;
894-
} else if (
895-
node.nodeName.toLowerCase() !== type.toLowerCase() ||
896-
isMarkedResource(node)
897-
) {
898-
// This is either text or a tag type that differs from the tag we are trying to hydrate
899-
// or a Node we already bound to a hoistable. We skip past it.
900-
continue;
901-
} else {
902-
// We have an Element with the right type.
903-
const element: Element = (node: any);
904-
905-
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
906-
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
907-
// using high entropy attributes for certain types. This technique will fail for strange insertions like
908-
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
909-
switch (type) {
910-
// case 'title':
911-
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
912-
// 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.
913-
case 'meta': {
914-
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
915-
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
916-
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
917-
// implications are minimal
918-
if (!element.hasAttribute('itemprop')) {
919-
// This is a Hoistable
920-
continue;
921-
}
922-
break;
923-
}
924-
case 'link': {
925-
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
926-
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
927-
// matches.
928-
const rel = element.getAttribute('rel');
929-
if (
930-
rel === 'stylesheet' &&
931-
element.hasAttribute('data-precedence')
932-
) {
933-
// This is a stylesheet resource
934-
continue;
935-
} else if (
936-
rel !== anyProps.rel ||
937-
element.getAttribute('href') !==
938-
(anyProps.href == null ? null : anyProps.href) ||
939-
element.getAttribute('crossorigin') !==
940-
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
941-
element.getAttribute('title') !==
942-
(anyProps.title == null ? null : anyProps.title)
943-
) {
944-
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
945-
// and title could vary for rel alternate
946-
continue;
947-
}
948-
break;
949-
}
950-
case 'style': {
951-
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
952-
// 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
953-
if (element.hasAttribute('data-precedence')) {
954-
// This is a style resource
955-
continue;
956-
}
957-
break;
958-
}
959-
case 'script': {
960-
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
961-
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
962-
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
963-
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
964-
// if we learn it is problematic
965-
const srcAttr = element.getAttribute('src');
966-
if (
967-
srcAttr &&
968-
element.hasAttribute('async') &&
969-
!element.hasAttribute('itemprop')
970-
) {
971-
// This is an async script resource
972-
continue;
973-
} else if (
974-
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
975-
element.getAttribute('type') !==
976-
(anyProps.type == null ? null : anyProps.type) ||
977-
element.getAttribute('crossorigin') !==
978-
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
979-
) {
980-
// This script is for a different src
981-
continue;
982-
}
983-
break;
984-
}
869+
): boolean {
870+
if (instance.nodeType !== ELEMENT_NODE) {
871+
// This is a suspense boundary or Text node.
872+
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
873+
// and this is a hydration error.
874+
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
875+
// but it seems reasonable and conservative to reject this as a hydration error as well
876+
return false;
877+
} else if (
878+
instance.nodeName.toLowerCase() !== type.toLowerCase() ||
879+
isMarkedResource(instance)
880+
) {
881+
// We are either about to
882+
return true;
883+
} else {
884+
// We have an Element with the right type.
885+
const element: Element = (instance: any);
886+
const anyProps = (props: any);
887+
888+
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
889+
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
890+
// using high entropy attributes for certain types. This technique will fail for strange insertions like
891+
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
892+
switch (type) {
893+
// case 'title':
894+
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
895+
// 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.
896+
case 'meta': {
897+
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
898+
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
899+
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
900+
// implications are minimal
901+
if (!element.hasAttribute('itemprop')) {
902+
// This is a Hoistable
903+
return true;
904+
}
905+
break;
906+
}
907+
case 'link': {
908+
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
909+
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
910+
// matches.
911+
const rel = element.getAttribute('rel');
912+
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
913+
// This is a stylesheet resource
914+
return true;
915+
} else if (
916+
rel !== anyProps.rel ||
917+
element.getAttribute('href') !==
918+
(anyProps.href == null ? null : anyProps.href) ||
919+
element.getAttribute('crossorigin') !==
920+
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
921+
element.getAttribute('title') !==
922+
(anyProps.title == null ? null : anyProps.title)
923+
) {
924+
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
925+
// and title could vary for rel alternate
926+
return true;
927+
}
928+
break;
929+
}
930+
case 'style': {
931+
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
932+
// 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
933+
if (element.hasAttribute('data-precedence')) {
934+
// This is a style resource
935+
return true;
936+
}
937+
break;
938+
}
939+
case 'script': {
940+
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
941+
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
942+
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
943+
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
944+
// if we learn it is problematic
945+
const srcAttr = element.getAttribute('src');
946+
if (
947+
srcAttr &&
948+
element.hasAttribute('async') &&
949+
!element.hasAttribute('itemprop')
950+
) {
951+
// This is an async script resource
952+
return true;
953+
} else if (
954+
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
955+
element.getAttribute('type') !==
956+
(anyProps.type == null ? null : anyProps.type) ||
957+
element.getAttribute('crossorigin') !==
958+
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
959+
) {
960+
// This script is for a different src
961+
return true;
985962
}
986-
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
987-
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
988-
// that should work in the vast majority of cases.
989-
return element;
963+
break;
990964
}
991965
}
966+
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
967+
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
968+
// that should work in the vast majority of cases.
969+
return false;
970+
}
971+
}
972+
973+
export function shouldSkipHydratableForTextInstance(
974+
instance: HydratableInstance,
975+
): boolean {
976+
return instance.nodeType === ELEMENT_NODE;
977+
}
978+
979+
export function shouldSkipHydratableForSuspenseInstance(
980+
instance: HydratableInstance,
981+
): boolean {
982+
return instance.nodeType === ELEMENT_NODE;
983+
}
984+
985+
export function canHydrateInstance(
986+
instance: HydratableInstance,
987+
type: string,
988+
props: Props,
989+
): null | Instance {
990+
if (
991+
instance.nodeType !== ELEMENT_NODE ||
992+
instance.nodeName.toLowerCase() !== type.toLowerCase()
993+
) {
992994
return null;
993995
} else {
994-
if (
995-
instance.nodeType !== ELEMENT_NODE ||
996-
instance.nodeName.toLowerCase() !== type.toLowerCase()
997-
) {
998-
return null;
999-
} else {
1000-
return ((instance: any): Instance);
1001-
}
996+
return ((instance: any): Instance);
1002997
}
1003998
}
1004999

1005-
export function getNextMatchingHydratableTextInstance(
1000+
export function canHydrateTextInstance(
10061001
instance: HydratableInstance,
10071002
text: string,
1008-
rootOrSingletonContext: boolean,
10091003
): null | TextInstance {
1010-
// We set this first because it must always be set on every invocation
1011-
hydratableNode = instance;
1012-
1013-
// Return early if there is nothing to hydrate (there will be no dom node if empty text)
10141004
if (text === '') return null;
10151005

1016-
if (rootOrSingletonContext) {
1017-
while (hydratableNode) {
1018-
const node = hydratableNode;
1019-
if (node.nodeType === COMMENT_NODE) {
1020-
// This is a suspense boundary we must halt here because we know this was not injected by 3rd party
1021-
return null;
1022-
} else if (node.nodeType !== TEXT_NODE) {
1023-
// Empty strings are not parsed by HTML so there won't be a correct match here.
1024-
hydratableNode = getNextHydratableSibling(node);
1025-
continue;
1026-
}
1027-
// This has now been refined to a text node.
1028-
return ((hydratableNode: any): TextInstance);
1029-
}
1030-
} else {
1031-
if (instance.nodeType !== TEXT_NODE) {
1032-
// Empty strings are not parsed by HTML so there won't be a correct match here.
1033-
return null;
1034-
}
1035-
// This has now been refined to a text node.
1036-
return ((instance: any): TextInstance);
1006+
if (instance.nodeType !== TEXT_NODE) {
1007+
// Empty strings are not parsed by HTML so there won't be a correct match here.
1008+
return null;
10371009
}
1038-
1039-
return null;
1010+
// This has now been refined to a text node.
1011+
return ((instance: any): TextInstance);
10401012
}
10411013

1042-
export function getNextMatchingHydratableSuspenseInstance(
1014+
export function canHydrateSuspenseInstance(
10431015
instance: HydratableInstance,
1044-
rootOrSingletonContext: boolean,
10451016
): null | SuspenseInstance {
1046-
// We set this first because it must always be set on every invocation
1047-
hydratableNode = instance;
1048-
1049-
if (rootOrSingletonContext) {
1050-
while (hydratableNode) {
1051-
if (hydratableNode.nodeType !== COMMENT_NODE) {
1052-
hydratableNode = getNextHydratableSibling(hydratableNode);
1053-
continue;
1054-
}
1055-
// This has now been refined to a suspense node.
1056-
return ((hydratableNode: any): SuspenseInstance);
1057-
}
1017+
if (instance.nodeType !== COMMENT_NODE) {
10581018
return null;
1059-
} else {
1060-
if (instance.nodeType !== COMMENT_NODE) {
1061-
return null;
1062-
}
1063-
// This has now been refined to a suspense node.
1064-
return ((instance: any): SuspenseInstance);
10651019
}
1020+
// This has now been refined to a suspense node.
1021+
return ((instance: any): SuspenseInstance);
10661022
}
10671023

10681024
export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {

0 commit comments

Comments
 (0)