@samitouri / QOS-React-2 / commits / 1cea384480

[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.

Josh Story committed May 30, 2023 at 13:12 UTC 1cea384480a6dea80128e5e0ddb714df7bea1520
2 files changed +26 -5
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+15
@@ -989,9 +989,23 @@ function clearContainerSparingly(container: Node) {
989 detachDeletedInstance(element);
990 continue;
991 }
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':
1005 case 'STYLE': {
1006 continue;
1007 }
1008 + // Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions
1009 case 'LINK': {
1010 if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
1011 continue;
@@ -1939,6 +1953,7 @@ export function clearSingleton(instance: Instance): void {
1953 isMarkedHoistable(node) ||
1954 nodeName === 'HEAD' ||
1955 nodeName === 'BODY' ||
1956 + nodeName === 'SCRIPT' ||
1957 nodeName === 'STYLE' ||
1958 (nodeName === 'LINK' &&
1959 ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+11 -5
@@ -87,12 +87,14 @@ describe('ReactDOM HostSingleton', () => {
87 let node = element.firstChild;
88 while (node) {
89 if (node.nodeType === 1) {
90 + const el: Element = (node: any);
91 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')
98 ) {
99 const props = {};
100 const attributes = node.attributes;
@@ -742,11 +744,13 @@ describe('ReactDOM HostSingleton', () => {
744 <link rel="stylesheet" href="headbefore" />
745 <title>this should be removed</title>
746 <link rel="stylesheet" href="headafter" />
747 + <script data-meaningful="">true</script>
748 </head>
749 <body>
750 <link rel="stylesheet" href="bodybefore" />
751 <div>this should be removed</div>
752 <link rel="stylesheet" href="bodyafter" />
753 + <script data-meaningful="">true</script>
754 </body>
755 </html>,
756 );
@@ -771,11 +775,13 @@ describe('ReactDOM HostSingleton', () => {
775 <head>
776 <link rel="stylesheet" href="headbefore" />
777 <link rel="stylesheet" href="headafter" />
778 + <script data-meaningful="">true</script>
779 <title>something new</title>
780 </head>
781 <body>
782 <link rel="stylesheet" href="bodybefore" />
783 <link rel="stylesheet" href="bodyafter" />
784 + <script data-meaningful="">true</script>
785 <div>something new</div>
786 </body>
787 </html>,