diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index db3e2806ac3e..a4ea1e24b9b0 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -5588,6 +5588,14 @@ export function acquireResource( props: any, ): null | Instance { resource.count++; + // If a previously-created instance has been removed from the document + // (e.g. because it lived inside a portal container that was later + // removed from the DOM), we must treat it as if it were never created. + // Otherwise React would reuse the disconnected node and the styles it + // represents would be permanently lost for the rest of the session. + if (resource.instance !== null && !resource.instance.isConnected) { + resource.instance = null; + } if (resource.instance === null) { switch (resource.type) { case 'style': { diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 21bf9684b285..25d0a1d90d57 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -8699,6 +8699,72 @@ background-color: green; , ); }); + + it('re-inserts a style resource when its portal container is removed from the DOM', async () => { + // Regression test for https://github.com/facebook/react/issues/36373: + // When a , + portalTarget, + )} + +
content
+ + ); + } + + const root = ReactDOMClient.createRoot(container); + await clientAct(() => { + root.render(); + }); + + // Both the portal render and the main-tree render share the same resource. + // After the first render the style should be in the document head. + const stylesBefore = document.head.querySelectorAll( + '[data-href="x"][data-precedence="custom"]', + ); + expect(stylesBefore.length).toBe(1); + + // Remove the portal target (simulates unmounting a detached portal container). + portalTarget.remove(); + + // Re-render to force a fresh acquireResource call with the same href. + await clientAct(() => { + setState(false); // hide portal, only main-tree style remains + }); + + // The style should still exist in the document — if the fix is absent, + // the cached-but-disconnected instance would be returned and the style + // would be gone from the document. + const stylesAfter = document.head.querySelectorAll( + '[data-href="x"][data-precedence="custom"]', + ); + expect(stylesAfter.length).toBeGreaterThanOrEqual(1); + + root.unmount(); + container.remove(); + }); }); describe('Script Resources', () => {