Repository: facebook/react
Pull request: [Fiber] retain scripts on `clearContainer` and `clearSingleton`

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.

Changed files:
filename: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
patch: @@ -989,9 +989,23 @@ function clearContainerSparingly(container: Node) {
         detachDeletedInstance(element);
         continue;
       }
+      // Script tags are retained to avoid an edge case bug. Normally scripts will execute if they
+      // are ever inserted into the DOM. However when streaming if a script tag is opened but not
+      // yet closed some browsers create and insert the script DOM Node but the script cannot execute
+      // yet until the closing tag is parsed. If something causes React to call clearContainer while
+      // this DOM node is in the document but not yet executable the DOM node will be removed from the
+      // document and when the script closing tag comes in the script will not end up running. This seems
+      // to happen in Chrome/Firefox but not Safari at the moment though this is not necessarily specified
+      // behavior so it could change in future versions of browsers. While leaving all scripts is broader
+      // than strictly necessary this is the least amount of additional code to avoid this breaking
+      // edge case.
+      //
+      // Style tags are retained because they may likely come from 3rd party scripts and extensions
+      case 'SCRIPT':
       case 'STYLE': {
         continue;
       }
+      // Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions
       case 'LINK': {
         if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
           continue;
@@ -1939,6 +1953,7 @@ export function clearSingleton(instance: Instance): void {
       isMarkedHoistable(node) ||
       nodeName === 'HEAD' ||
       nodeName === 'BODY' ||
+      nodeName === 'SCRIPT' ||
       nodeName === 'STYLE' ||
       (nodeName === 'LINK' &&
         ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')

filename: packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
patch: @@ -87,12 +87,14 @@ describe('ReactDOM HostSingleton', () => {
     let node = element.firstChild;
     while (node) {
       if (node.nodeType === 1) {
+        const el: Element = (node: any);
         if (
-          node.tagName !== 'SCRIPT' &&
-          node.tagName !== 'TEMPLATE' &&
-          node.tagName !== 'template' &&
-          !node.hasAttribute('hidden') &&
-          !node.hasAttribute('aria-hidden')
+          (el.tagName !== 'SCRIPT' &&
+            el.tagName !== 'TEMPLATE' &&
+            el.tagName !== 'template' &&
+            !el.hasAttribute('hidden') &&
+            !el.hasAttribute('aria-hidden')) ||
+          el.hasAttribute('data-meaningful')
         ) {
           const props = {};
           const attributes = node.attributes;
@@ -742,11 +744,13 @@ describe('ReactDOM HostSingleton', () => {
             <link rel="stylesheet" href="headbefore" />
             <title>this should be removed</title>
             <link rel="stylesheet" href="headafter" />
+            <script data-meaningful="">true</script>
           </head>
           <body>
             <link rel="stylesheet" href="bodybefore" />
             <div>this should be removed</div>
             <link rel="stylesheet" href="bodyafter" />
+            <script data-meaningful="">true</script>
           </body>
         </html>,
       );
@@ -771,11 +775,13 @@ describe('ReactDOM HostSingleton', () => {
         <head>
           <link rel="stylesheet" href="headbefore" />
           <link rel="stylesheet" href="headafter" />
+          <script data-meaningful="">true</script>
           <title>something new</title>
         </head>
         <body>
           <link rel="stylesheet" href="bodybefore" />
           <link rel="stylesheet" href="bodyafter" />
+          <script data-meaningful="">true</script>
           <div>something new</div>
         </body>
       </html>,