Skip to content

Commit 1cea384

Browse files
authored
[Fiber] retain scripts on clearContainer and clearSingleton (#26871)
clearContainer and clearSingleton both assumed scripts could be safely removed from the DOM because normally once a script has been inserted into the DOM it is executable and removing it, even synchronously, will not prevent it from running. However There is an edge case in a couple browsers (Chrome at least) where during HTML streaming if a script is opened and not yet closed the script will be inserted into the document but not yet executed. If the script is removed from the document before the end tag is parsed then the script will not run. This change causes clearContainer and clearSingleton to retain script elements. This is generally thought to be safe because if we are calling these methods we are no longer hydrating the container or the singleton and the scripts execution will happen regardless.
1 parent a1f9758 commit 1cea384

File tree

2 files changed

+26
-5
lines changed

2 files changed

+26
-5
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,9 +989,23 @@ function clearContainerSparingly(container: Node) {
989989
detachDeletedInstance(element);
990990
continue;
991991
}
992+
// Script tags are retained to avoid an edge case bug. Normally scripts will execute if they
993+
// are ever inserted into the DOM. However when streaming if a script tag is opened but not
994+
// yet closed some browsers create and insert the script DOM Node but the script cannot execute
995+
// yet until the closing tag is parsed. If something causes React to call clearContainer while
996+
// this DOM node is in the document but not yet executable the DOM node will be removed from the
997+
// document and when the script closing tag comes in the script will not end up running. This seems
998+
// to happen in Chrome/Firefox but not Safari at the moment though this is not necessarily specified
999+
// behavior so it could change in future versions of browsers. While leaving all scripts is broader
1000+
// than strictly necessary this is the least amount of additional code to avoid this breaking
1001+
// edge case.
1002+
//
1003+
// Style tags are retained because they may likely come from 3rd party scripts and extensions
1004+
case 'SCRIPT':
9921005
case 'STYLE': {
9931006
continue;
9941007
}
1008+
// Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions
9951009
case 'LINK': {
9961010
if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
9971011
continue;
@@ -1939,6 +1953,7 @@ export function clearSingleton(instance: Instance): void {
19391953
isMarkedHoistable(node) ||
19401954
nodeName === 'HEAD' ||
19411955
nodeName === 'BODY' ||
1956+
nodeName === 'SCRIPT' ||
19421957
nodeName === 'STYLE' ||
19431958
(nodeName === 'LINK' &&
19441959
((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')

packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,14 @@ describe('ReactDOM HostSingleton', () => {
8787
let node = element.firstChild;
8888
while (node) {
8989
if (node.nodeType === 1) {
90+
const el: Element = (node: any);
9091
if (
91-
node.tagName !== 'SCRIPT' &&
92-
node.tagName !== 'TEMPLATE' &&
93-
node.tagName !== 'template' &&
94-
!node.hasAttribute('hidden') &&
95-
!node.hasAttribute('aria-hidden')
92+
(el.tagName !== 'SCRIPT' &&
93+
el.tagName !== 'TEMPLATE' &&
94+
el.tagName !== 'template' &&
95+
!el.hasAttribute('hidden') &&
96+
!el.hasAttribute('aria-hidden')) ||
97+
el.hasAttribute('data-meaningful')
9698
) {
9799
const props = {};
98100
const attributes = node.attributes;
@@ -742,11 +744,13 @@ describe('ReactDOM HostSingleton', () => {
742744
<link rel="stylesheet" href="headbefore" />
743745
<title>this should be removed</title>
744746
<link rel="stylesheet" href="headafter" />
747+
<script data-meaningful="">true</script>
745748
</head>
746749
<body>
747750
<link rel="stylesheet" href="bodybefore" />
748751
<div>this should be removed</div>
749752
<link rel="stylesheet" href="bodyafter" />
753+
<script data-meaningful="">true</script>
750754
</body>
751755
</html>,
752756
);
@@ -771,11 +775,13 @@ describe('ReactDOM HostSingleton', () => {
771775
<head>
772776
<link rel="stylesheet" href="headbefore" />
773777
<link rel="stylesheet" href="headafter" />
778+
<script data-meaningful="">true</script>
774779
<title>something new</title>
775780
</head>
776781
<body>
777782
<link rel="stylesheet" href="bodybefore" />
778783
<link rel="stylesheet" href="bodyafter" />
784+
<script data-meaningful="">true</script>
779785
<div>something new</div>
780786
</body>
781787
</html>,

0 commit comments

Comments
 (0)