diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 2fcf5bf9a57ff..7dd5b923bcc23 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -2411,7 +2411,7 @@ export function getResource(
if (!resource) {
// We asserted this above but Flow can't figure out that the type satisfies
const ownerDocument = getDocumentFromRoot(resourceRoot);
- resource = {
+ resource = ({
type: 'stylesheet',
instance: null,
count: 0,
@@ -2419,15 +2419,34 @@ export function getResource(
loading: NotLoaded,
preload: null,
},
- };
+ }: StylesheetResource);
styles.set(key, resource);
+ const instance = ownerDocument.querySelector(
+ getStylesheetSelectorFromKey(key),
+ );
+ if (instance) {
+ const loadingState: ?Promise = (instance: any)._p;
+ if (loadingState) {
+ // This instance is inserted as part of a boundary reveal and is not yet
+ // loaded
+ } else {
+ // This instance is already loaded
+ resource.instance = instance;
+ resource.state.loading = Loaded | Inserted;
+ }
+ }
+
if (!preloadPropsMap.has(key)) {
- preloadStylesheet(
- ownerDocument,
- key,
- preloadPropsFromStylesheet(qualifiedProps),
- resource.state,
- );
+ const preloadProps = preloadPropsFromStylesheet(qualifiedProps);
+ preloadPropsMap.set(key, preloadProps);
+ if (!instance) {
+ preloadStylesheet(
+ ownerDocument,
+ key,
+ preloadProps,
+ resource.state,
+ );
+ }
}
}
return resource;
@@ -2520,28 +2539,21 @@ function preloadStylesheet(
preloadProps: PreloadProps,
state: StylesheetState,
) {
- preloadPropsMap.set(key, preloadProps);
-
- if (!ownerDocument.querySelector(getStylesheetSelectorFromKey(key))) {
- // There is no matching stylesheet instance in the Document.
- // We will insert a preload now to kick off loading because
- // we expect this stylesheet to commit
- const preloadEl = 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;
- } else {
- const instance = ownerDocument.createElement('link');
- state.preload = instance;
- instance.addEventListener('load', () => (state.loading |= Loaded));
- instance.addEventListener('error', () => (state.loading |= Errored));
- setInitialProperties(instance, 'link', preloadProps);
- markNodeAsHoistable(instance);
- (ownerDocument.head: any).appendChild(instance);
- }
+ const preloadEl = 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;
+ } else {
+ const instance = ownerDocument.createElement('link');
+ state.preload = instance;
+ instance.addEventListener('load', () => (state.loading |= Loaded));
+ instance.addEventListener('error', () => (state.loading |= Errored));
+ setInitialProperties(instance, 'link', preloadProps);
+ markNodeAsHoistable(instance);
+ (ownerDocument.head: any).appendChild(instance);
}
}
diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
index 419817b52e1f3..5e632aa36d48e 100644
--- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
+++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
@@ -45,6 +45,11 @@ export function completeBoundaryWithStyles(
const dependencies = [];
let href, precedence, attr, loadingState, resourceEl, media;
+ function cleanupWith(cb) {
+ this['_p'] = null;
+ cb();
+ }
+
// Sheets Mode
let sheetMode = true;
while (true) {
@@ -80,18 +85,14 @@ export function completeBoundaryWithStyles(
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
- resourceEl.onload = resolve;
- resourceEl.onerror = reject;
+ resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
+ resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
});
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
- if (
- loadingState &&
- loadingState['s'] !== 'l' &&
- (!media || window['matchMedia'](media).matches)
- ) {
+ if (loadingState && (!media || window['matchMedia'](media).matches)) {
dependencies.push(loadingState);
}
if (avoidInsert) {
diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
index 1b0ba47378357..6e47c3b0658e3 100644
--- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
@@ -6,7 +6,7 @@ export const clientRenderBoundary =
export const completeBoundary =
'$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
export const completeBoundaryWithStyles =
- '$RM=new Map;\n$RR=function(r,t,w){for(var u=$RC,n=$RM,p=new Map,q=document,g,b,h=q.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&n.set(b.getAttribute("href"),b),p.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var f=w[b++];if(!f){k=!1;b=0;continue}var c=!1,m=0;var d=f[m++];if(a=n.get(d)){var e=a._p;c=!0}else{a=q.createElement("link");a.href=d;a.rel="stylesheet";for(a.dataset.precedence=\nl=f[m++];e=f[m++];)a.setAttribute(e,f[m++]);e=a._p=new Promise(function(x,y){a.onload=x;a.onerror=y});n.set(d,a)}d=a.getAttribute("media");!e||"l"===e.s||d&&!matchMedia(d).matches||h.push(e);if(c)continue}else{a=v[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=p.get(l)||g;c===g&&(g=a);p.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=q.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(u.bind(null,r,t,""),u.bind(null,r,t,"Resource failed to load"))};';
+ '$RM=new Map;\n$RR=function(t,u,y){function v(n){this._p=null;n()}for(var w=$RC,p=$RM,q=new Map,r=document,g,b,h=r.querySelectorAll("link[data-precedence],style[data-precedence]"),x=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?x.push(b):("LINK"===b.tagName&&p.set(b.getAttribute("href"),b),q.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=y[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=p.get(d)){var f=a._p;c=!0}else{a=r.createElement("link");a.href=\nd;a.rel="stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,z){a.onload=v.bind(a,n);a.onerror=v.bind(a,z)});p.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=x[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=q.get(l)||g;c===g&&(g=a);q.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=r.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(w.bind(null,\nt,u,""),w.bind(null,t,u,"Resource failed to load"))};';
export const completeSegment =
'$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};';
export const formReplaying =
diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js
index 511ee1db3fd9a..abf17903e3e83 100644
--- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js
+++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js
@@ -47,6 +47,11 @@ export function completeBoundaryWithStyles(
const dependencies = [];
let href, precedence, attr, loadingState, resourceEl, media;
+ function cleanupWith(cb) {
+ this['_p'] = null;
+ cb();
+ }
+
// Sheets Mode
let sheetMode = true;
while (true) {
@@ -82,18 +87,14 @@ export function completeBoundaryWithStyles(
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
- resourceEl.onload = resolve;
- resourceEl.onerror = reject;
+ resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
+ resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
});
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
- if (
- loadingState &&
- loadingState['s'] !== 'l' &&
- (!media || window['matchMedia'](media).matches)
- ) {
+ if (loadingState && (!media || window['matchMedia'](media).matches)) {
dependencies.push(loadingState);
}
if (avoidInsert) {
diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
index 43926bfa160fe..01a6d6f13727f 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
@@ -3348,6 +3348,172 @@ body {
);
});
+ it('will assume stylesheets already in the document have loaded if it cannot confirm it is not yet loaded', async () => {
+ await act(() => {
+ renderToPipeableStream(
+
+
+
+
+
+
+
+ ,
+ ).pipe(writable);
+ });
+
+ const root = ReactDOMClient.createRoot(document.querySelector('#foo'));
+
+ root.render(
+
+
+
+ hello world
+
+
,
+ );
+
+ await waitForAll([]);
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ it('will assume wait for loading stylesheets to load before continuing', async () => {
+ let ssr = true;
+ function Component() {
+ if (ssr) {
+ return null;
+ } else {
+ return (
+ <>
+
+ hello client
+ >
+ );
+ }
+ }
+
+ await act(() => {
+ renderToPipeableStream(
+
+
+
+
+
+
+ hello world
+
+
+
+
+
+
+
+
+
+ ,
+ ).pipe(writable);
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+ loading...
+
+
+ ,
+ );
+
+ await act(() => {
+ resolveText('reveal');
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+ loading...
+
+
+
+ ,
+ );
+
+ ssr = false;
+
+ ReactDOMClient.hydrateRoot(
+ document,
+
+
+
+
+
+
+ hello world
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ await waitForAll([]);
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+ loading...
+
+
+
+ ,
+ );
+
+ await expect(async () => {
+ loadStylesheets();
+ }).toErrorDev([
+ "Hydration failed because the server rendered HTML didn't match the client.",
+ ]);
+ assertLog(['load stylesheet: foo']);
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
it('can suspend commits on more than one root for the same resource at the same time', async () => {
document.body.innerHTML = '';
const container1 = document.createElement('div');