From 6a0e25329b93441ee0f97893c4592334cc58618b Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 30 May 2023 10:39:39 -0700 Subject: [PATCH] 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. --- .../src/client/ReactFiberConfigDOM.js | 15 +++++++++++++++ .../ReactDOMSingletonComponents-test.js | 16 +++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 11c290d1481c..f3441a3557e3 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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') diff --git a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js index f8056fd6042e..4d34f2edb7d5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js @@ -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', () => { this should be removed +
this should be removed
+ , ); @@ -771,11 +775,13 @@ describe('ReactDOM HostSingleton', () => { + something new +
something new
,