diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js b/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js index 01e4fcde95cb..8438410e03a0 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js @@ -50,6 +50,7 @@ const internalEventHandlesSetKey = '__reactHandles$' + randomKey; const internalRootNodeResourcesKey = '__reactResources$' + randomKey; const internalHoistableMarker = '__reactMarker$' + randomKey; const internalScrollTimer = '__reactScroll$' + randomKey; +const internalLoadPendingKey = '__reactLoad$' + randomKey; type InstanceUnion = | Instance @@ -386,6 +387,18 @@ export function clearScrollEndTimer(node: EventTarget): void { (node: any)[internalScrollTimer] = undefined; } +export function markNodeAsPendingLoad(node: Node): void { + (node: any)[internalLoadPendingKey] = true; +} + +export function clearPendingLoadOnNode(node: Node): void { + (node: any)[internalLoadPendingKey] = undefined; +} + +export function isNodePendingLoad(node: Node): boolean { + return (node: any)[internalLoadPendingKey] === true; +} + export function isOwnedInstance(node: Node): boolean { if (enableInternalInstanceMap) { return !!( diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index db3e2806ac3e..1ae2f663c72c 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -56,6 +56,9 @@ import { getResourcesFromRoot, isMarkedHoistable, markNodeAsHoistable, + markNodeAsPendingLoad, + clearPendingLoadOnNode, + isNodePendingLoad, isOwnedInstance, } from './ReactDOMComponentTree'; import { @@ -5005,11 +5008,18 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) { as === 'script' && ownerDocument.querySelector(getScriptSelectorFromKey(key)) ) { - // We already have a stylesheet for this key. We don't need to preload it. + // We already have a script for this key. We don't need to preload it. return; } const instance = ownerDocument.createElement('link'); setInitialProperties(instance, 'link', preloadProps); + if (as === 'style') { + // Stash a loading state on the preload link. it will clean itself up once settled + markNodeAsPendingLoad(instance); + instance.onload = instance.onerror = () => { + clearPendingLoadOnNode(instance); + }; + } markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } @@ -5357,19 +5367,16 @@ export function getResource( resource.instance = instance; resource.state.loading = Loaded | Inserted; } - } - - if (!preloadPropsMap.has(key)) { - const preloadProps = preloadPropsFromStylesheet(qualifiedProps); - preloadPropsMap.set(key, preloadProps); - if (!instance) { - preloadStylesheet( - ownerDocument, - key, - preloadProps, - resource.state, - ); + } else { + // We don't have an instance we need to preload it instead + // $FlowFixMe[incompatible-type] -- the key we use here can only match non module preloads + let preloadProps: void | PreloadProps = preloadPropsMap.get(key); + if (!preloadProps) { + preloadProps = preloadPropsFromStylesheet(qualifiedProps); + preloadPropsMap.set(key, preloadProps); } + + preloadStylesheet(ownerDocument, key, preloadProps, resource.state); } } if (currentProps && currentResource === null) { @@ -5540,22 +5547,33 @@ function preloadStylesheet( preloadProps: PreloadProps, state: StylesheetState, ) { - const preloadEl = ownerDocument.querySelector( + let instance = ownerDocument.querySelector( getPreloadStylesheetSelectorFromKey(key), ); - if (preloadEl) { - // If we find a preload already it was SSR'd and we won't have an actual - // loading state to track. For now we will just assume it is loaded - state.loading = Loaded; + if (instance) { + if (!isNodePendingLoad(instance)) { + // If we find a preload already it was SSR'd and we won't have an actual + // loading state to track. For now we will just assume it is loaded + state.loading = Loaded; + return; + } else { + // fall through and attach loading listeners + } } else { - const instance = ownerDocument.createElement('link'); - state.preload = instance; - instance.addEventListener('load', () => (state.loading |= Loaded)); - instance.addEventListener('error', () => (state.loading |= Errored)); + instance = ownerDocument.createElement('link'); + markNodeAsPendingLoad(instance); + instance.onload = instance.onerror = clearPendingLoadOnNode.bind( + null, + instance, + ); setInitialProperties(instance, 'link', preloadProps); markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } + // $FlowFixMe: [incompatible-type] -- if instance is an Element it will also be an HTMLLinkElement + state.preload = instance; + instance.addEventListener('load', () => (state.loading |= Loaded)); + instance.addEventListener('error', () => (state.loading |= Errored)); } function preloadPropsFromStylesheet( diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 21bf9684b285..4207fa5e32e0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -3674,6 +3674,110 @@ body { ); }); + it('does not suspend a transition on a stylesheet whose preload has already loaded', async () => { + const root = ReactDOMClient.createRoot(document); + root.render( + +
+